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
Add vendor bundling of blocks frontend bundle to improve cart/checkout performance #45859
Conversation
Hi @sunyatasattva, Apart from reviewing the code changes, please make sure to review the testing instructions as well. You can follow this guide to find out what good testing instructions should look like: |
Test Results SummaryCommit SHA: 3688f6b
To view the full API test report, click here. To view the full E2E test report, click here. To view all test reports, visit the WooCommerce Test Reports Dashboard. |
…mmerce-blocks, woocommerce
name: 'wc-blocks-vendors', | ||
chunks: ( chunk ) => { | ||
return ( | ||
chunk.name !== 'product-button-interactivity' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason, turning on vendor bundling in the product-button-interactivity
bundle caused some imports of that bundle to be tree-shaken. I've opted just not to vendor bundle for that at the moment. It was the only place there was an issue. The chunk is loaded in the shop page anyway so won't have an impact on cart and checkout. Also there will be some refactoring to remove the dependency on @wordpress/components
that this has (due to using the woo notice component), when that comes up i'll revisit if this one can (or needs) to be further optimized.
EDIT, actually its worth mentioning that once @wordpress/components
is removed from this block, there won't be anything significant to vendor bundle anyway. Apart from that it's very, very small.
@@ -378,20 +383,17 @@ const getFrontConfig = ( options = {} ) => { | |||
minSize: 200000, | |||
automaticNameDelimiter: '--', | |||
cacheGroups: { | |||
...getCacheGroups(), | |||
'base-components': { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was duplicated from an identical entry generated by getCacheGroups
so this is tidy up.
@@ -327,7 +331,8 @@ const getFrontConfig = ( options = {} ) => { | |||
// @see https://github.com/Automattic/jetpack/pull/20926 | |||
chunkFilename: `[name]-frontend${ fileSuffix }.js?ver=[contenthash]`, | |||
filename: `[name]-frontend${ fileSuffix }.js`, | |||
uniqueName: 'webpackWcBlocksJsonp', | |||
uniqueName: 'webpackWcBlocksFrontendJsonp', | |||
library: [ 'wc', '[name]' ], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Webpack DEWP is actually non-functional in most of the bundles right now, because you must set a library export name if you want dependencies to be externalized. By setting this we enable DEWP on this bundle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@samueljseay can you help me understand this? What does it mean for DEWP to be non-functional in the bundles? As in it's not resolving imports e.g. @woocommerce/blocks-checkout
to window.wc.blocksCheckout
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@opr yes sorry this is sort of misleading, I think I made a mistake saying it's non-functional. It's just that without this, it won't externalize any dependency to the window that is declared in this bundle.
Maybe that was intentional originally, to just have it load things from the window but not need to externalize anything itself in bundles outside of the "main" bundle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Sam, ah I see what you mean, so anything we want to put onto the winow.wc
wasn't going there from this package?
@@ -327,7 +331,8 @@ const getFrontConfig = ( options = {} ) => { | |||
// @see https://github.com/Automattic/jetpack/pull/20926 | |||
chunkFilename: `[name]-frontend${ fileSuffix }.js?ver=[contenthash]`, | |||
filename: `[name]-frontend${ fileSuffix }.js`, | |||
uniqueName: 'webpackWcBlocksJsonp', | |||
uniqueName: 'webpackWcBlocksFrontendJsonp', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I initially started optimizing the build i got runtime errors because we mistakenly had the same unique name for different bundles. This is a no-no. If 2 bundles on the same page have same unique name they can accidentally load packages from each others bundles and this was happening to me. The front end bundle would try load from the main vendor bundle rather than the frontend vendor bundle, causing issues if some parts of a dependency were tree shaken in that vendor bundle but not in mine. I think for consistency each entry needs a unique name to avoid this kind of conflict in future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sense, I don't see any issues with changing the names to be unique.
@@ -236,6 +234,10 @@ const entries = { | |||
frontend: { | |||
reviews: './assets/js/blocks/reviews/frontend.ts', | |||
...getBlockEntries( 'frontend.{t,j}s{,x}' ), | |||
|
|||
blocksCheckout: './packages/checkout/index.js', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move these entry points to the frontend bundle.
await beanieAddToCartButton.click(); | ||
|
||
// Add to cart initiates a request that could be interrupted by navigation, wait till it's done. | ||
await expect( beanieAddToCartButton ).toHaveText( /in winkelwagen/ ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests had a race condition that was exposed, I guess by slight changes in the bundling. If you navigate before the request to add to cart has finished you might interrupt it, so this waits for that to ensure these pass.
@@ -61,8 +61,9 @@ public function register_assets() { | |||
// The price package is shared externally so has no blocks prefix. | |||
$this->api->register_script( 'wc-price-format', 'assets/client/blocks/price-format.js', array(), false ); | |||
|
|||
$this->api->register_script( 'wc-blocks-checkout', 'assets/client/blocks/blocks-checkout.js', array() ); | |||
$this->api->register_script( 'wc-blocks-components', 'assets/client/blocks/blocks-components.js', array() ); | |||
$this->api->register_script( 'wc-blocks-vendors-frontend', $this->api->get_block_asset_build_path( 'wc-blocks-vendors-frontend' ), array(), false ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add the new vendor bundle as a dependency to blocks and checkout scripts.
I'm not sure I understand this, does it mean that they have a lot of shared things that you combined into the vendor script? |
@senadir not very well worded on my part, and probably incomplete explanation. What I'm trying to say is that moving blocks-checkout and blocks-components to the frontend bundle allows us to get the most benefit from vendor bundles because within the bundle they were in they don't share so many dependencies with other entrypoints, but rather introduce a lot of dependencies just for those 2 files, but those dependencies they introduce happen to in many cases already be being pulled in to the frontend bundle for the other entrypoints there. So when we bundle them in frontend bundle we get the maximum kb savings from vendor bundling there. (We can't share vendor scripts across bundles either, i think you know that but just mentioning it for anyone who isn't familiar with Webpack). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on these improvements, @samueljseay! I could see the bundle size improvements in the Cart and the Checkout pages and they are pretty impressive, good job!
I just have a couple of comments/questions:
- The bundle size seems to have increased in some other simpler blocks. Ie: I tested adding the Reviews by Category and the All Products blocks in two different pages. When comparing
trunk
with this branch, it looks like the total bundle size to render those blocks has increased slightly. I don't think it's a big deal, as this seems to only happen when pages have one or very few blocks and most of that extra size comes fromblocks-vendors-frontend
so it will be cached across pages. But wanted to mention anyway. 🙂 - If I'm understanding the code right, we are assuming all frontend scripts will have a dependency on either
blocks-checkout
orblocks-components
and, because of that,blocks-vendors-frontend
will be enqueued. However, some time in the future there might be a block with a frontend script that imports from anode_modules
package but doesn't import fromblocks-checkout
norblocks-components
. In that case, we would need to manually declareblocks-vendors-frontend
as a dependency, is that correct?
This brings me to the third question. This PR extracts the vendor scripts from all frontend packages and creates the wc-blocks-vendors-frontend
script. I wonder if you considered doing the same but instead of bundling the vendor scripts of all frontend packages doing it only for the blocks-checkout
and blocks-components
scripts. In other words, instead of using the frontend
Webpack entry, we would create a new separate one for blocks-checkout
and blocks-components
. I have no idea if that would make much sense or if you had already evaluated that and discarded for some reason, but wanted to mention it to know your thoughts.
None of these comments are blockers neither suggestions to change, I'm mostly asking them to better understand why we prefer this approach. Also keep in mind it was a long time since I worked on the Webpack config so I might be missing a lot of context. 😅
Btw, thanks for adding inline comments explaining the code changes, they were super helpful when reviewing the PR!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@samueljseay thanks a lot for working on this! The changes you've made make sense to me, and I tested the Cart/Checkout blocks, I also tested some of the shop/add to cart flow, such as filter blocks but my work there was not exhaustive.
Before merging this, I propose we invite Rubik/Kirigami/Origami to really test the flows their team is responsible using various different settings.
Yes thanks for calling this out, you're right that if a standalone entry with very few dependencies from the vendor bundle loads it will be overall larger than before. As you said, vendor is cached across pages, but it certainly is a trade-off. In future we could split off more bundles if we can find a logical way to separate significant vendor dependencies.
Exactly. And I think this problem also exists with the other bundle that uses a vendor script too (can't remember off top of my head if its core or main bundle). If you don't consider that your entrypoint has dependency on
The reason imo not to do this, is because even though some small dependencies do get a little bigger when they share almost no dependencies with the vendors script, is that most of the scripts in the frontend bundle still benefit. There are many inner blocks of the cart and checkout pages that get loaded and before they were duplicating the same dependencies over and over. The cumulative payload optimization is not just the reduction in size of blocks-components and blocks-checkout but also in entrypoints such I have more ideas for optimization of our bundles though. A timely update of the wordpress polyfill for example should shed quite a lot of weight from all our frontend packages, and the follow up of replacing wordpress components with ariakit will make things smaller again. |
@opr I agree! 💯 I'll put out a call for testing this next week. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the in-depth explanation, @samueljseay! ❤️ It makes a lot of sense, I see some trade-offs need to be taken, but I agree that overall this is an improvement to what we have right now.
I'm approving from my side, but it would be great to get more people to review and test it to make sure everything works well.
This has me wondering about the potential impact on extenders that have written scripts that a dependency on any specific block frontend scripts for individual blocks prior to this change. If those individual frontend scripts already had a dependency of |
Thanks @samueljseay for the great work! I have smoke tested all the non-C&C blocks on the editor-side and played around with various settings. No broken behaviour or runtime errors detected. Of course, I didn't go the deepest possible, but I played around with pretty much everything non-C&C. |
Its a good point. I couldn't think of too many situations where you'd end up in this position without the vendor scripts loading, but I agree even the small possibility of an edge case requires that we communicate this. I will look at ensuring we get a dev note included in the next release and I agree we can also showcase the size reduction in C&C especially. 🙏🏻 |
@opr I have done some testing of the C&C. I explored the UI from front-end and editor, I adjusted settings of inner blocks within the C&C and saw no runtime issues or odd behaviours. |
I'm merging this now. We need to keep a close eye on any issues springing up on this between now and next release. |
Changes proposed in this Pull Request:
Recently I discovered while reviewing the cart and checkout webpack builds that there was a relatively low-hanging opportunity for optimization.
Specifically we don't do any vendor bundling in the front-end webpack bundle. There are so many entrypoints in this bundle, so even one small dependency being duplicated across all of them, drastically impacts the payload size for cart and checkout. This is especially true because checkout for example, has many inner blocks that are all entrypoints which will all end up duplicating any vendor dependency.
Furthermore, the
blocks-checkout
andblocks-components
scripts that were being built in a separate bundle were not able to make use of vendor bundling because most of their dependencies were duplicated from the front-end bundle.So to solve this, I moved those bundles across to the front-end bundle and turned on vendor bundling of node_modules there. On a production build, in testing, this reduced the payload of the cart page by
11%
and the checkout page by17.9%
.Closes #45680
How to test the changes in this Pull Request:
I am leaning in on the e2e tests here to see that this still works. For tester though you should do a smoke test of:
And also you should test all of the above in the editor as well. Try changing block settings, add / remove the blocks and ensure that no JavaScript exceptions have been thrown.
Changelog entry
Significance
Type
Message
Introduce vendor bundling to the blocks cart and checkout pages to improve performance.
Comment