Skip to content
This repository was archived by the owner on Sep 30, 2025. It is now read-only.

Conversation

emma-boardman
Copy link
Contributor

@emma-boardman emma-boardman commented May 6, 2021

WHY are these changes introduced?

Prevents listeners being detached / reattached on every rerender

WHAT is this pull request doing?

  • Includes react-hooks as a dependency
    • useIsomorphicEffect is in v1.13.0
  • assigns handler event to a ref
  • wraps handleKeyEvent in useCallback
  • leverages useIsomorphicLayoutEffect to update handlerRef
  • adds useEffect dependency array

@emma-boardman emma-boardman self-assigned this May 6, 2021
@github-actions
Copy link
Contributor

github-actions bot commented May 6, 2021

🟡 This pull request modifies 5 files and might impact 63 other files. This is an average splash zone for a change, remember to tophat areas that could be affected.

Details:
All files potentially affected (total: 63)
📄 .storybook/main.js (total: 0)

Files potentially affected (total: 0)

📄 UNRELEASED.md (total: 0)

Files potentially affected (total: 0)

📄 package.json (total: 0)

Files potentially affected (total: 0)

🧩 src/components/KeypressListener/KeypressListener.tsx (total: 63)

Files potentially affected (total: 63)

📄 yarn.lock (total: 0)

Files potentially affected (total: 0)

@emma-boardman
Copy link
Contributor Author

emma-boardman commented May 6, 2021

Storybook error:

ERROR in ./node_modules/@shopify/react-hooks/build/esm/hooks/debounced.mjs 21:18-26
Can't import the named export 'useState' from non EcmaScript module (only default export is available)
 @ ./node_modules/@shopify/react-hooks/build/esm/hooks/index.mjs
 @ ./node_modules/@shopify/react-hooks/build/esm/index.mjs
 @ ./node_modules/@shopify/react-hooks/index.mjs
 @ ./src/components/KeypressListener/KeypressListener.tsx
 @ ./src/components/KeypressListener/index.ts
 @ ./src/components/index.ts
 @ ./src/index.ts
 @ ./.storybook/preview.js
 @ ./.storybook/preview.js-generated-config-entry.js
 @ multi ./node_modules/@storybook/core/dist/server/common/polyfills.js ./node_modules/@storybook/core/dist/server/preview/globals.js ./.storybook/storybook-init-framework-entry.js ./node_modules/@storybook/addon-docs/dist/frameworks/common/config.js-generated-other-entry.js ./node_modules/@storybook/addon-docs/dist/frameworks/react/config.js-generated-other-entry.js ./node_modules/@storybook/addon-actions/dist/preset/addDecorator.js-generated-other-entry.js ./node_modules/@storybook/addon-actions/dist/preset/addArgs.js-generated-other-entry.js ./node_modules/@storybook/addon-backgrounds/dist/preset/addDecorator.js-generated-other-entry.js ./node_modules/@storybook/addon-backgrounds/dist/preset/addParameter.js-generated-other-entry.js ./node_modules/@storybook/addon-a11y/dist/a11yRunner.js-generated-other-entry.js ./node_modules/@storybook/addon-a11y/dist/a11yHighlight.js-generated-other-entry.js ./node_modules/@storybook/addon-knobs/dist/preset/addDecorator.js-generated-other-entry.js ./.storybook/preview.js-generated-config-entry.js ./.storybook/generated-stories-entry.js ./node_modules/webpack-hot-middleware/client.js?reload=true&quiet=false&noInfo=true

Screenshot 2021-05-06 at 12 00 07

Investigation notes:

  • Can't replicate in OSUI Storybook if I bump the react-hooks dependency to 1.13.0 and include useIsomorphicLayoutEffect.
    • OSUI storybook-react is v6.2.7
    • Polaris storybook-react is v6.1.11

Resolution attempt #1: Try bumping polaris storybook-react to 1.13.0

Error message
ModuleBuildError: Module build failed (from ./node_modules/@storybook/builder-webpack4/node_modules/babel-loader/lib/index.js):
SyntaxError: /Users/emmaboardman/src/github.com/Shopify/polaris-react/src/components/Sticky/Sticky.tsx: Missing class properties transform.
  26 |
  27 | class StickyInner extends Component<CombinedProps, State> {
> 28 |   state: State = {
     |   ^
  29 |     isSticky: false,
  30 |     style: {},
  31 |   };
    at File.buildCodeFrameError (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/@storybook/builder-webpack4/node_modules/@babel/core/lib/transformation/file/file.js:240:12)
    at NodePath.buildCodeFrameError (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/@storybook/builder-webpack4/node_modules/@babel/traverse/lib/path/index.js:138:21)
    at pushBody (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/@babel/plugin-transform-classes/lib/transformClass.js:161:20)
    at buildBody (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/@babel/plugin-transform-classes/lib/transformClass.js:135:5)
    at classTransformer (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/@babel/plugin-transform-classes/lib/transformClass.js:544:5)
    at transformClass (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/@babel/plugin-transform-classes/lib/transformClass.js:580:10)
    at PluginPass.ClassExpression (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/@babel/plugin-transform-classes/lib/index.js:63:54)
    at newFn (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/@storybook/builder-webpack4/node_modules/@babel/traverse/lib/visitors.js:175:21)
    at NodePath._call (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/@storybook/builder-webpack4/node_modules/@babel/traverse/lib/path/context.js:55:20)
    at NodePath.call (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/@storybook/builder-webpack4/node_modules/@babel/traverse/lib/path/context.js:42:17)
    at NodePath.visit (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/@storybook/builder-webpack4/node_modules/@babel/traverse/lib/path/context.js:92:31)
    at TraversalContext.visitQueue (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/@storybook/builder-webpack4/node_modules/@babel/traverse/lib/context.js:116:16)
    at TraversalContext.visitSingle (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/@storybook/builder-webpack4/node_modules/@babel/traverse/lib/context.js:85:19)
    at TraversalContext.visit (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/@storybook/builder-webpack4/node_modules/@babel/traverse/lib/context.js:144:19)
    at Function.traverse.node (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/@storybook/builder-webpack4/node_modules/@babel/traverse/lib/index.js:82:17)
    at NodePath.visit (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/@storybook/builder-webpack4/node_modules/@babel/traverse/lib/path/context.js:99:18)
    at runLoaders (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/webpack/lib/NormalModule.js:316:20)
    at /Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/loader-runner/lib/LoaderRunner.js:367:11
    at /Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/loader-runner/lib/LoaderRunner.js:233:18
    at context.callback (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/loader-runner/lib/LoaderRunner.js:111:13)
    at loader.call.then.err (/Users/emmaboardman/src/github.com/Shopify/polaris-react/node_modules/@storybook/builder-webpack4/node_modules/babel-loader/lib/index.js:59:103)

@emma-boardman emma-boardman marked this pull request as ready for review May 6, 2021 17:31
@emma-boardman emma-boardman requested review from kaelig and clauderic May 6, 2021 17:31
Comment on lines 120 to 86
config.module.rules.push({
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto',
});
Copy link
Member

Choose a reason for hiding this comment

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

Could you leave context on the PR as to how this relates to the change you made / why it was required? 🙏

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This corrected the storybook error documented here: #4173 (comment)

@kaelig are you able to elaborate on why we need to add this?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm actually not sure this is the best way to fix this, but when I looked into ways to get you unblocked, this happened to work! I'd love to look deeper into the why and how this works, I'm but I'm short on time right now :(

Copy link
Member

@BPScott BPScott May 7, 2021

Choose a reason for hiding this comment

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

This will work for within polaris's storybook but it means that every consuming app needs to add this to their webpack config too to include polaris, which is not acceptable. Partners using create-react-app / next.js / anything that isn't sewing-kit will be impacted and that area of effect is way too high.

This will be a breaking change for a several consumers, please don't do it.

This is a sign that react-hooks isn't shipping valid strict esm code. That should be fixed at source. The good news is I've been working on changing quilt's build system to rollup, which I i believe might help with this, if you want to sit tight until that work is ready - https://github.com/Shopify/sewing-kit-next/pull/147 / https://github.com/Shopify/sewing-kit-next/issues/137.

BPScott
BPScott previously requested changes May 7, 2021
Copy link
Member

@BPScott BPScott left a comment

Choose a reason for hiding this comment

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

That webpack fix is not the right one, as it any consuming app will need to make it too. Thus this is a breaking change for lots of consumers which is bad.

I've put a possible solution inline.

Comment on lines 120 to 86
config.module.rules.push({
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto',
});
Copy link
Member

@BPScott BPScott May 7, 2021

Choose a reason for hiding this comment

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

This will work for within polaris's storybook but it means that every consuming app needs to add this to their webpack config too to include polaris, which is not acceptable. Partners using create-react-app / next.js / anything that isn't sewing-kit will be impacted and that area of effect is way too high.

This will be a breaking change for a several consumers, please don't do it.

This is a sign that react-hooks isn't shipping valid strict esm code. That should be fixed at source. The good news is I've been working on changing quilt's build system to rollup, which I i believe might help with this, if you want to sit tight until that work is ready - https://github.com/Shopify/sewing-kit-next/pull/147 / https://github.com/Shopify/sewing-kit-next/issues/137.

@BPScott
Copy link
Member

BPScott commented May 7, 2021

Alternatively you could avoid the react-hooks dependency by recreating the hook as it is trivial (this is probably nicer than adding a new dependency).

Turns out we do this already in Autocomplete's combobox: https://github.com/Shopify/polaris-react/blob/main/src/components/Autocomplete/components/ComboBox/ComboBox.tsx#L74

The source for the hook (to compare against what Autocomplete does; https://github.com/Shopify/quilt/blob/main/packages/react-hooks/src/hooks/isomorphic-layout-effect.ts

polaris-react store its own hooks in the src/utilities folder (e.g. src/utilities/use-toggle.tsx)

[keyCode],
);

useIsomorphicLayoutEffect(() => {
Copy link
Member

@BPScott BPScott May 7, 2021

Choose a reason for hiding this comment

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

I don't think I understand the need for this here anyway? I would have expected that the memoisation of handleKeyEvent and specifying that as a dependency of the useEffect would have been enough to stop the removal/re-adding anyway? - as the handleKeyEvent function would no longer be recreated on every render of the component, and thus no longer trigger the useEffect on every render

(written but not tested)

 const handleKeyEvent = useCallback((event: KeyboardEvent) => {
    if (event.keyCode === keyCode) {
      handler(event);
    }
  }), [handler, keyCode];

useEffect(() => {
    document.addEventListener(keyEvent, handleKeyEvent);
    return () => {
      document.removeEventListener(keyEvent, handleKeyEvent);
    };
  });
  }, [keyEvent, handleKeyEvent]);

Copy link
Member

@clauderic clauderic May 10, 2021

Choose a reason for hiding this comment

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

@BPScott the snippet you have above delegates the responsibility of ensuring that the handler prop is memoized to the consumer of KeypressListener, otherwise the useEffect callback will run on every single render if handler is not memoized (since it is a dependency of handleKeyEvent, which is a dependency of the effect).

Components such as KeypressListener shouldn't assume that their consumers will always remember to memoize the handlers they pass to them, and should optimize for their consumers instead when it makes sense to do so. In this case it's fairly low overhead for KeypressListener to do this for its consumers and will drastically reduce the number of times listeners are detached/re-attached

The pattern introduced in this PR ensures that listeners aren't attached and re-attached on every single render regardless of whether the consumer remembers to memoize their handlers

@emma-boardman emma-boardman force-pushed the event-listeners-improve-performance branch 2 times, most recently from b489e1c to 14aee29 Compare May 11, 2021 09:52
@emma-boardman emma-boardman requested a review from BPScott May 11, 2021 10:13
@emma-boardman emma-boardman force-pushed the event-listeners-improve-performance branch from 94bf25b to 8d797bb Compare May 12, 2021 16:42
@emma-boardman emma-boardman requested a review from clauderic May 12, 2021 16:43
@emma-boardman emma-boardman force-pushed the event-listeners-improve-performance branch from 8d797bb to f08c5e8 Compare May 13, 2021 11:16
@emma-boardman
Copy link
Contributor Author

@BPScott I'm getting failures in the git workflow. Is that something to be worried about? It passes the PR CI builds 🤔

.github/workflows/size-limit.yml#L1size is not a valid event name

@emma-boardman emma-boardman force-pushed the event-listeners-improve-performance branch from d363c6f to 22f0015 Compare May 26, 2021 10:40
@github-actions
Copy link
Contributor

github-actions bot commented May 26, 2021

size-limit report

Path Size
cjs 141.88 KB (+0.02% 🔺)
esm 95.49 KB (+0.07% 🔺)
esnext 138.66 KB (+0.04% 🔺)
css 33.72 KB (0%)

Copy link
Member

@BPScott BPScott left a comment

Choose a reason for hiding this comment

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

Thanks for putting up with my slow/busyness. My original concerns have been addressed.

I've not ran the code to confirm behaviour, but I'm no longer blocking this :)

@BPScott BPScott dismissed their stale review June 1, 2021 15:33

concens addressed

@emma-boardman
Copy link
Contributor Author

@kaelig

I created some performance flame charts, on main and on this branch. However, I'm not sure how indicative they are of the memoization impact. In both instances EventListener is < 0.1ms of 0.1ms, even with multiple listeners rendering. The rendered at figures for the feature branch look smaller, for what that's worth.

Does this help, or are there other ways I can measure the performance impact?

Main Feature
main with multiple branch with multiple

@emma-boardman emma-boardman force-pushed the event-listeners-improve-performance branch from 22f0015 to 1263948 Compare June 3, 2021 11:35
@kaelig
Copy link
Contributor

kaelig commented Jun 3, 2021

Thank you for looking into this! The React devtools flamecharts only show components. You'll need to look at the Chrome DevTools flamecharts to see more granular data and you can also compare the number of resulting event listeners, possibly even looking at memory footprint impact when encountering a worst case scenario (very big listener function, lots of renders)... But that starts to get a bit overkill so you probably don't have to go that deep.

Feel free to spend 30 more minutes at most on this — the PR looks good on principle and I thought flamecharts would have been a nice addition but they're not a requirement!

Copy link
Contributor

@kaelig kaelig left a comment

Choose a reason for hiding this comment

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

I haven't been able to assess the real world impact of such performance improvements but I support this PR on principle :shipit:

@emma-boardman
Copy link
Contributor Author

Thank you for looking into this! The React devtools flamecharts only show components. You'll need to look at the Chrome DevTools flamecharts to see more granular data and you can also compare the number of resulting event listeners, possibly even looking at memory footprint impact when encountering a worst case scenario (very big listener function, lots of renders)... But that starts to get a bit overkill so you probably don't have to go that deep.

Feel free to spend 30 more minutes at most on this — the PR looks good on principle and I thought flamecharts would have been a nice addition but they're not a requirement!

I ran some experiments in Chrome devtools, and I couldn't see any evidence for this having reduced the number of resulting event listeners 😬 However, I'm probably doing it wrong 😅 I'm going to merge this and dig more into effective performance testing at a later date! Thank you for the review and for bringing my attention to flamecharts and profiling 😁

@emma-boardman emma-boardman merged commit f31aee1 into main Jun 4, 2021
@emma-boardman emma-boardman deleted the event-listeners-improve-performance branch June 4, 2021 13:26
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants