Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⚡ Performance improvements when using spread in reduce #2937

Merged
merged 15 commits into from
May 27, 2024

Conversation

KenAJoh
Copy link
Collaborator

@KenAJoh KenAJoh commented May 16, 2024

Description

As a test im using Biome to do some linting and slowly fixing some of the errors found.

This PR is based around the rule: https://biomejs.dev/linter/rules/no-accumulating-spread/

In real applications these changes will have marginal effect for our use since we rarely handle large arrays. The only exception i see is combobox/FilteredOptions/filteredOptionsContext.tsx where one in theory could end up having arrays in the thousand or more. (but again the performance-issues from just having 10k+ dom nodes would probably be the real problem long before this)

Made a small test-script to compare performance

Screenshot 2024-05-16 at 13 54 49
const { performance } = require("perf_hooks");

function createArray(length) {
  return new Array(length).fill(0).map((_, i) => i + 1);
}

function testPerformance(array, useSpread) {
  const start = performance.now();

  array.reduce((acc, val) => {
    if (useSpread) {
      return [...acc, val];
    } else {
      acc.push(val);
      return acc;
    }
  }, []);

  const end = performance.now();

  return (end - start).toFixed(3);
}

function main() {
  const lengths = [10, 100, 1000, 10000, 100000];

  for (const length of lengths) {
    const array = createArray(length);

    const timeWithSpread = testPerformance(array, true);
    console.log(`Time with spread for length ${length}: ${timeWithSpread}ms`);

    const timeWithoutSpread = testPerformance(array, false);
    console.log(
      `Time without spread for length ${length}: ${timeWithoutSpread}ms`,
    );
  }
}

main();

Copy link

changeset-bot bot commented May 16, 2024

🦋 Changeset detected

Latest commit: e5ca8c3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 7 packages
Name Type
@navikt/ds-react Patch
@navikt/ds-css Patch
@navikt/ds-tokens Patch
@navikt/ds-tailwind Patch
@navikt/aksel-icons Patch
@navikt/aksel Patch
@navikt/aksel-stylelint Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Contributor

github-actions bot commented May 16, 2024

Storybook demo

d7147cb7f | 81 komponenter | 182 stories

@KenAJoh KenAJoh enabled auto-merge (squash) May 16, 2024 11:59
Comment on lines 19 to 23
const globalRefs = Object.entries(docs)
.filter(([key]) => key.startsWith("global-"))
.reduce(
(acc, [, value]) => [...acc, ...value],
[] as { name: string; value: string | number }[],
.flatMap(([, value]) =>
value.map(({ name, value: _value }) => ({ name, value: String(_value) })),
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some weird reason, name becomes any now 🙃

I found that one way of fixing it is to add type to Object.entries. I also think we can remove the nested map():

  const globalRefs = Object.entries<
    { name: string; value: string | number; description?: string }[]
  >(docs)
    .filter(([key]) => key.startsWith("global-"))
    .flatMap(([, value]) => value);

This is a bit ugly though, so you probably want to extract the type.

It might be that it's actually cleaner to use map() and flat() separately in this case, b.c. then we don't get that type issue:

  const globalRefs = Object.entries(docs)
    .filter(([key]) => key.startsWith("global-"))
    .map(([, value]) => value)
    .flat();

But maybe you can find a better way to fix that type bug...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cound not find a better way to preserve types, but biome complains when not using flatmap, so had to "hack" it by adding the flat() in the return

const globalRefs = Object.entries(docs)
    .filter(([key]) => key.startsWith("global-"))
    .map(([, value]) => value);

  return (
    globalRefs
      .flat()
      .find(
        ({ value, name }) =>
          semanticValue === value && notBlacklistedName(name),
      ) ?? null
  );

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like you forgot to push the changes, so I will do it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have a changeset for Combobox? 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we follow the semver strategy stictly this would be a patch, but for such small internal changes i often feel it could just hitch a ride on the next update we push. But if we want as a team we could force all changes to be patches in these scenarios. What does everyone think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think small refactoring should be fine to do without needing a changeset, since the changeset is mostly for external eyes, and I guess they would mostly care about features and bugfixes, (maybe performance stuff), but plain refactoring gives users nothing right? (if they inspect our code it might be more readable).

@@ -83,29 +83,27 @@ const FilteredOptionsProvider = ({
const [isMouseLastUsedInputDevice, setIsMouseLastUsedInputDevice] =
useState(false);

const filteredOptionsMap = useMemo(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HalvorHaugan you mean add a changeset to combobox for this change right?

In this case it's a only a refactoring right? (pulling out initialMap from within the reduce())

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was what I meant, yes. Mainly bc. it's a performance improvement. If it was only a plain refactor that no-one would notice, I would not have a changeset for it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I'm for adding it! ⚡ (I didn't realize it was perf in that instance, but that's the whole PR's purpose 😳 )

@KenAJoh KenAJoh merged commit 61de685 into main May 27, 2024
3 checks passed
@KenAJoh KenAJoh deleted the biome-linter-fixes branch May 27, 2024 09:14
@github-actions github-actions bot mentioned this pull request May 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants