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

perf[react-native]: Memoized Blocks Component to free up UI thread. #3814

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

yash-builder
Copy link

@yash-builder yash-builder commented Jan 13, 2025

Description

Suspense:
Added React.Suspense to defer the rendering of the Content component until it is fully loaded.

Memoization:
React.memo to memoize computationally expensive operations within the Blocks component. To prevents unnecessary recalculations of processed blocks during re-renders, reducing the load on the UI thread.

Screenshot
Before:
Screenshot 2025-01-13 at 12 20 38 PM

After:
Screenshot 2025-01-13 at 12 37 41 PM

Copy link

changeset-bot bot commented Jan 13, 2025

⚠️ No Changeset found

Latest commit: bb5b126

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

Copy link

nx-cloud bot commented Jan 13, 2025

View your CI Pipeline Execution ↗ for commit bb5b126.

Command Status Duration Result
nx test @e2e/nextjs-sdk-next-app ✅ Succeeded 7m 50s View ↗
nx test @e2e/nuxt ✅ Succeeded 7m 13s View ↗
nx test @e2e/qwik-city ✅ Succeeded 7m 25s View ↗
nx test @e2e/react-sdk-next-pages ✅ Succeeded 6m 5s View ↗
nx test @e2e/react-sdk-next-14-app ✅ Succeeded 5m 57s View ↗
nx test @e2e/react-native ✅ Succeeded 5m 31s View ↗
nx test @e2e/react-sdk-next-15-app ✅ Succeeded 5m 37s View ↗
nx test @e2e/sveltekit ✅ Succeeded 5m 8s View ↗
Additional runs (34) ✅ Succeeded ... View ↗

☁️ Nx Cloud last updated this comment at 2025-01-22 08:56:30 UTC

Copy link
Contributor

@midhunadarvin midhunadarvin left a comment

Choose a reason for hiding this comment

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

Awesome work @yash-builder

));

// Optional: Give the component a display name for better debugging
RenderBlock.displayName = 'RenderBlock';
Copy link
Contributor

Choose a reason for hiding this comment

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

🎯 suggestion:
Should we append block.id to the displayName as a identifier ?

Copy link
Author

Choose a reason for hiding this comment

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

We can surely do that. But in this scenario I used this identifier for inspecting the component in React Devtools

};

// Moved outside and memoized
const RenderBlock = memo(({
Copy link
Contributor

Choose a reason for hiding this comment

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

question:
We have used memo over Block component ( to get RenderBlock ) and then composed the RenderBlock with React.useCallback. Could you explain the logic here and how it works internally ?

  • what happens if we use memo / useCallback in both places ?

Copy link
Author

Choose a reason for hiding this comment

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

Great question!

  • If Only memo Is Used:
    RenderBlock will avoid re-renders, but FlatList may still recreate the renderItem function on every render. This may lead to potential performance bottleneck.
  • If Only useCallback Is Used:
    The renderItem function will be stable, but RenderBlock will re-render if its props are shallowly different, even when not required.

examples/nextjs-app-dir-v2/package.json Show resolved Hide resolved
Copy link
Contributor

@samijaber samijaber left a comment

Choose a reason for hiding this comment

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

Very well done! I'm Rewriting here what I sent you in Slack so we don't lose track of it.

One important thing is not to use overrides/* unless as a last resort because the file will quickly go out of sync with the original Blocks.lite.tsx, forcing us to maintain multiple copies of the same logic.

Here is a Loom showing how I'd tackle the ~3-4 changes in your PR and incorporate them into Mitosis plugins instead:

https://www.loom.com/share/94f1767796bd487bb48be41d9046e01f

Also, you need to add a changesets entry, which you can do by following https://github.com/BuilderIO/builder/blob/main/packages/sdks/PUBLISHING.md

Comment on lines 223 to 233
code: {
post: (code, json) => {
if (json.name === 'Blocks') {
return code.replace(
'export default Blocks',
'export default memo(Blocks)'
);
}
return code;
},
},
Copy link
Contributor

Choose a reason for hiding this comment

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

Initially in this PR, you were memoizing Block, but now you're memoizing Blocks instead.

Was this intended? Do we still get the same performance benefits from memoizing the parent component instead of the individual items?

Copy link
Author

Choose a reason for hiding this comment

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

Hey Sami, In the earlier PR also we were memoizing Blocks.tsx and not Block.tsx

Copy link
Contributor

@samijaber samijaber Jan 21, 2025

Choose a reason for hiding this comment

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

Ah indeed, you memoized Blocks.tsx in your original PR commit.

but you were also creating a memoized version of Block called RenderBlock : https://github.com/BuilderIO/builder/pull/3814/commits/e264c120fd38851208875a9915b55e26e952528f#diff-b9cbe94aa51d49deda0[…]fd13d38a1f56e5641441f5R20

CleanShot 2025-01-21 at 07 25 26@2x

Which is now gone from this PR. Was it unnecessary?

Copy link
Author

Choose a reason for hiding this comment

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

Ah my bad I missed this part. Also one question I want to ask by using mitosis how can I write code outside the Blocks function but inside the Blocks.tsx

Copy link
Contributor

Choose a reason for hiding this comment

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

in my Loom I explan that in this case, you probably want to add export default Memo(Block) to Block.tsx, and remove this RenderBlock from Blocks.tsx to achieve the same outcome

Copy link
Author

Choose a reason for hiding this comment

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

gotcha!
Asking out of curiosity. Is there a way where we can declare variables/functions outside Functional/Class component using mitosis.config.js?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes actually, there's a preComponent hook in Mitosis whose code will be printed right after import statements (and therefore, outside React components)

Copy link
Contributor

@samijaber samijaber left a comment

Choose a reason for hiding this comment

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

I see large-reactive-state.spec.ts is failing for React Native now. This is an integration stress-test for SDK performance: it renders thousands of interactive elements on the same page, performs multiple state updates and makes sure it all gets done in a reasonable time frame.

It's a bit surprising that your PR is causing it to fail. Can you investigate this failure and make sure there aren't any unintended performance drawbacks to this solution?

@yash-builder
Copy link
Author

I see large-reactive-state.spec.ts is failing for React Native now. This is an integration stress-test for SDK performance: it renders thousands of interactive elements on the same page, performs multiple state updates and makes sure it all gets done in a reasonable time frame.

It's a bit surprising that your PR is causing it to fail. Can you investigate this failure and make sure there aren't any unintended performance drawbacks to this solution?

Hmm...that's weird let me take a look into it

Copy link
Contributor

@samijaber samijaber left a comment

Choose a reason for hiding this comment

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

Code looks good! You're still missing the changesets entry https://github.com/BuilderIO/builder/blob/main/packages/sdks/PUBLISHING.md#1--add-changeset

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