Create a separate bundle for cart and checkout (#48010)

This commit is contained in:
Sam Seay 2024-07-10 17:23:17 +08:00 committed by GitHub
parent a862dab4f7
commit eaf57e9f15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 290 additions and 32 deletions

View File

@ -0,0 +1,75 @@
// The Dependency Extraction Webpack Plugin does not add split chunks as dependencies
// to the .asset.php files that list dependencies. In the past we manually enqueued
// those dependencies in PHP.
// For every generated .asset.php file in the whole bundle, this plugin prefixes the
// list of dependencies with the handles of split chunks that were generated in the build.
// e.g. if your bundle has a vendors script called foo-vendors then for every entry-point
// that has a .asset.php file generated by Dependency Extraction Webpack Plugin
// the plugin will edit that file to include foo-vendors as a listed dependency.
// This means for any split chunk you build you'll only need to register it in PHP, but all
// files that depend on it will automatically include it as a dependency.
class AddSplitChunkDependencies {
apply( compiler ) {
compiler.hooks.thisCompilation.tap(
'AddStableChunksToAssets',
( compilation, callback ) => {
compilation.hooks.processAssets.tap(
{
name: 'AddStableChunksToAssets',
stage: compiler.webpack.Compilation
.PROCESS_ASSETS_STAGE_ANALYSE,
},
() => {
const { chunks } = compilation;
const splitChunks = chunks.filter( ( chunk ) => {
return chunk?.chunkReason?.includes( 'split' );
} );
// find files that have an asset.php file
const chunksToAddSplitsTo = chunks.filter(
( chunk ) => {
return (
! chunk?.chunkReason?.includes( 'split' ) &&
chunk.files.find( ( file ) =>
file.endsWith( 'asset.php' )
)
);
}
);
for ( const chunk of chunksToAddSplitsTo ) {
const assetFile = chunk.files.find( ( file ) =>
file.endsWith( 'asset.php' )
);
const assetFileContent = compilation.assets[
assetFile
]
.source()
.toString();
const extraDependencies = splitChunks
.map( ( c ) => `'${ c.name }'` )
.join( ', ' );
const updatedFileContent = assetFileContent.replace(
/('dependencies'\s*=>\s*array\s*\(\s*)([^)]*)\)/,
`$1${ extraDependencies }, $2)`
);
compilation.assets[ assetFile ] = {
source: () => updatedFileContent,
size: () => updatedFileContent.length,
};
}
}
);
}
);
}
}
module.exports = AddSplitChunkDependencies;

View File

@ -28,6 +28,7 @@ const {
getProgressBarPluginConfig,
getCacheGroups,
} = require( './webpack-helpers' );
const AddSplitChunkDependencies = require( './add-split-chunk-dependencies' );
const isProduction = NODE_ENV === 'production';
@ -330,17 +331,6 @@ const getFrontConfig = ( options = {} ) => {
// @see https://github.com/Automattic/jetpack/pull/20926
chunkFilename: `[name]-frontend${ fileSuffix }.js?ver=[contenthash]`,
filename: ( pathData ) => {
// blocksCheckout and blocksComponents were moved from core bundle,
// retain their filenames to avoid breaking translations.
if (
pathData.chunk.name === 'blocksCheckout' ||
pathData.chunk.name === 'blocksComponents'
) {
return `${ paramCase(
pathData.chunk.name
) }${ fileSuffix }.js`;
}
return `[name]-frontend${ fileSuffix }.js`;
},
uniqueName: 'webpackWcBlocksFrontendJsonp',
@ -395,9 +385,10 @@ const getFrontConfig = ( options = {} ) => {
minSize: 200000,
automaticNameDelimiter: '--',
cacheGroups: {
commons: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'wc-blocks-vendors',
// Note that filenames are suffixed with `frontend` so the generated file is `wc-blocks-frontend-vendors-frontend`.
name: 'wc-blocks-frontend-vendors',
chunks: ( chunk ) => {
return (
chunk.name !== 'product-button-interactivity'
@ -431,6 +422,7 @@ const getFrontConfig = ( options = {} ) => {
bundleAnalyzerReportTitle: 'Frontend',
} ),
new ProgressBarPlugin( getProgressBarPluginConfig( 'Frontend' ) ),
new AddSplitChunkDependencies(),
],
resolve: {
...resolve,
@ -986,6 +978,151 @@ const getInteractivityAPIConfig = ( options = {} ) => {
};
};
const getCartAndCheckoutFrontendConfig = ( options = {} ) => {
let { fileSuffix } = options;
const { alias, resolvePlugins = [] } = options;
fileSuffix = fileSuffix ? `-${ fileSuffix }` : '';
const resolve = alias
? {
alias,
plugins: resolvePlugins,
}
: {
plugins: resolvePlugins,
};
return {
entry: getEntryConfig(
'cartAndCheckoutFrontend',
options.exclude || []
),
output: {
devtoolNamespace: 'wc',
path: path.resolve( __dirname, '../build/' ),
// This is a cache busting mechanism which ensures that the script is loaded via the browser with a ?ver=hash
// string. The hash is based on the built file contents.
// @see https://github.com/webpack/webpack/issues/2329
// Using the ?ver string is needed here so the filename does not change between builds. The WordPress
// i18n system relies on the hash of the filename, so changing that frequently would result in broken
// translations which we must avoid.
// @see https://github.com/Automattic/jetpack/pull/20926
chunkFilename: `[name]-frontend${ fileSuffix }.js?ver=[contenthash]`,
filename: ( pathData ) => {
// blocksCheckout and blocksComponents were moved from core bundle,
// retain their filenames to avoid breaking translations.
if (
pathData.chunk.name === 'blocksCheckout' ||
pathData.chunk.name === 'blocksComponents'
) {
return `${ paramCase(
pathData.chunk.name
) }${ fileSuffix }.js`;
}
return `[name]-frontend${ fileSuffix }.js`;
},
uniqueName: 'webpackWcBlocksCartCheckoutFrontendJsonp',
library: [ 'wc', '[name]' ],
},
module: {
rules: [
{
test: /\.(j|t)sx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
[
'@wordpress/babel-preset-default',
{
modules: false,
targets: {
browsers: [
'extends @wordpress/browserslist-config',
],
},
},
],
],
plugins: [
isProduction
? require.resolve(
'babel-plugin-transform-react-remove-prop-types'
)
: false,
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-class-properties',
].filter( Boolean ),
cacheDirectory: true,
},
},
},
{
test: /\.s[c|a]ss$/,
use: {
loader: 'ignore-loader',
},
},
],
},
optimization: {
concatenateModules:
isProduction && ! process.env.WP_BUNDLE_ANALYZER,
splitChunks: {
minSize: 200000,
automaticNameDelimiter: '--',
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'wc-cart-checkout-vendors',
chunks: 'all',
enforce: true,
},
base: {
// A refined include blocks and settings that are shared between cart and checkout that produces the smallest possible bundle.
test: /assets[\\/]js[\\/](settings|previews|base|data|utils|blocks[\\/]cart-checkout-shared|icons)|packages[\\/](checkout|components)|atomic[\\/]utils/,
name: 'wc-cart-checkout-base',
chunks: 'all',
enforce: true,
},
...getCacheGroups(),
},
},
minimizer: [
new TerserPlugin( {
parallel: true,
terserOptions: {
output: {
comments: /translators:/i,
},
compress: {
passes: 2,
},
mangle: {
reserved: [ '__', '_n', '_nx', '_x' ],
},
},
extractComments: false,
} ),
],
},
plugins: [
...getSharedPlugins( {
bundleAnalyzerReportTitle: 'Cart & Checkout Frontend',
} ),
new ProgressBarPlugin(
getProgressBarPluginConfig( 'Cart & Checkout Frontend' )
),
new AddSplitChunkDependencies(),
],
resolve: {
...resolve,
extensions: [ '.js', '.ts', '.tsx' ],
},
};
};
module.exports = {
getCoreConfig,
getFrontConfig,
@ -995,4 +1132,5 @@ module.exports = {
getSiteEditorConfig,
getStylingConfig,
getInteractivityAPIConfig,
getCartAndCheckoutFrontendConfig,
};

View File

@ -25,9 +25,7 @@ const blocks = {
},
'attribute-filter': {},
breadcrumbs: {},
cart: {},
'catalog-sorting': {},
checkout: {},
'coming-soon': {},
'customer-account': {},
'featured-category': {
@ -43,10 +41,6 @@ const blocks = {
customDir: 'classic-template',
},
'classic-shortcode': {},
'mini-cart': {},
'mini-cart-contents': {
customDir: 'mini-cart/mini-cart-contents',
},
'store-notices': {},
'page-content-wrapper': {},
'price-filter': {},
@ -169,12 +163,22 @@ const blocks = {
},
};
// Intentional separation of cart and checkout entry points to allow for better code splitting.
const cartAndCheckoutBlocks = {
cart: {},
checkout: {},
'mini-cart': {},
'mini-cart-contents': {
customDir: 'mini-cart/mini-cart-contents',
},
};
// Returns the entries for each block given a relative path (ie: `index.js`,
// `**/*.scss`...).
// It also filters out elements with undefined props and experimental blocks.
const getBlockEntries = ( relativePath ) => {
const getBlockEntries = ( relativePath, blockEntries = blocks ) => {
return Object.fromEntries(
Object.entries( blocks )
Object.entries( blockEntries )
.map( ( [ blockCode, config ] ) => {
const filePaths = glob.sync(
`./assets/js/blocks/${ config.customDir || blockCode }/` +
@ -206,7 +210,10 @@ const entries = {
'./assets/js/atomic/blocks/product-elements/product-details/index.tsx',
'add-to-cart-form':
'./assets/js/atomic/blocks/product-elements/add-to-cart-form/index.tsx',
...getBlockEntries( '{index,block,frontend}.{t,j}s{,x}' ),
...getBlockEntries( '{index,block,frontend}.{t,j}s{,x}', {
...blocks,
...cartAndCheckoutBlocks,
} ),
// Interactivity component styling
'wc-interactivity-checkbox-list':
@ -239,17 +246,14 @@ const entries = {
'wc-blocks': './assets/js/index.js',
// Blocks
...getBlockEntries( 'index.{t,j}s{,x}' ),
...getBlockEntries( 'index.{t,j}s{,x}', {
...blocks,
...cartAndCheckoutBlocks,
} ),
},
frontend: {
reviews: './assets/js/blocks/reviews/frontend.ts',
...getBlockEntries( 'frontend.{t,j}s{,x}' ),
blocksCheckout: './packages/checkout/index.js',
blocksComponents: './packages/components/index.ts',
'mini-cart-component':
'./assets/js/blocks/mini-cart/component-frontend.tsx',
'product-button-interactivity':
'./assets/js/atomic/blocks/product-elements/button/frontend.tsx',
},
@ -273,6 +277,13 @@ const entries = {
'wc-blocks-classic-template-revert-button':
'./assets/js/templates/revert-button/index.tsx',
},
cartAndCheckoutFrontend: {
...getBlockEntries( 'frontend.{t,j}s{,x}', cartAndCheckoutBlocks ),
blocksCheckout: './packages/checkout/index.js',
blocksComponents: './packages/components/index.ts',
'mini-cart-component':
'./assets/js/blocks/mini-cart/component-frontend.tsx',
},
};
const getEntryConfig = ( type = 'main', exclude = [] ) => {

View File

@ -11,6 +11,7 @@ const {
getSiteEditorConfig,
getStylingConfig,
getInteractivityAPIConfig,
getCartAndCheckoutFrontendConfig,
} = require( './bin/webpack-configs.js' );
// Only options shared between all configs should be defined here.
@ -34,6 +35,11 @@ const sharedConfig = {
devtool: NODE_ENV === 'development' ? 'source-map' : false,
};
const CartAndCheckoutFrontendConfig = {
...sharedConfig,
...getCartAndCheckoutFrontendConfig( { alias: getAlias() } ),
};
// Core config for shared libraries.
const CoreConfig = {
...sharedConfig,
@ -95,6 +101,7 @@ const SiteEditorConfig = {
};
module.exports = [
CartAndCheckoutFrontendConfig,
CoreConfig,
MainConfig,
FrontendConfig,

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Create a separate JS cart and checkout JavaScript bundle to improve performance.

View File

@ -40,6 +40,7 @@ final class AssetsController {
add_action( 'admin_enqueue_scripts', array( $this, 'update_block_style_dependencies' ), 20 );
add_action( 'wp_enqueue_scripts', array( $this, 'update_block_settings_dependencies' ), 100 );
add_action( 'admin_enqueue_scripts', array( $this, 'update_block_settings_dependencies' ), 100 );
add_filter( 'js_do_concat', array( $this, 'skip_boost_minification_for_cart_checkout' ), 10, 2 );
}
/**
@ -62,9 +63,14 @@ final class AssetsController {
// 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-vendors-frontend', $this->api->get_block_asset_build_path( 'wc-blocks-vendors-frontend' ), array(), false );
$this->api->register_script( 'wc-blocks-checkout', 'assets/client/blocks/blocks-checkout.js', array( 'wc-blocks-vendors-frontend' ) );
$this->api->register_script( 'wc-blocks-components', 'assets/client/blocks/blocks-components.js', array( 'wc-blocks-vendors-frontend' ) );
// Vendor scripts for blocks frontends (not including cart and checkout).
$this->api->register_script( 'wc-blocks-frontend-vendors', $this->api->get_block_asset_build_path( 'wc-blocks-frontend-vendors-frontend' ), array(), false );
// Cart and checkout frontend scripts.
$this->api->register_script( 'wc-cart-checkout-vendors', $this->api->get_block_asset_build_path( 'wc-cart-checkout-vendors-frontend' ), array(), false );
$this->api->register_script( 'wc-cart-checkout-base', $this->api->get_block_asset_build_path( 'wc-cart-checkout-base-frontend' ), array(), false );
$this->api->register_script( 'wc-blocks-checkout', 'assets/client/blocks/blocks-checkout.js' );
$this->api->register_script( 'wc-blocks-components', 'assets/client/blocks/blocks-components.js' );
// Register the interactivity components here for now.
$this->api->register_script( 'wc-interactivity-dropdown', 'assets/client/blocks/wc-interactivity-dropdown.js', array() );
@ -253,6 +259,23 @@ final class AssetsController {
return $src;
}
/**
* Skip Jetpack Boost minification on older versions of Jetpack Boost where it causes issues.
*
* @param mixed $do_concat Whether to concatenate the script or not.
* @param mixed $handle The script handle.
* @return mixed
*/
public function skip_boost_minification_for_cart_checkout( $do_concat, $handle ) {
$boost_is_outdated = defined( 'JETPACK_BOOST_VERSION' ) && version_compare( JETPACK_BOOST_VERSION, '3.4.2', '<' );
$scripts_to_ignore = [
'wc-cart-checkout-vendors',
'wc-cart-checkout-base',
];
return $boost_is_outdated && in_array( $handle, $scripts_to_ignore, true ) ? false : $do_concat;
}
/**
* Add body classes to the frontend and within admin.
*