Webpack启用SplitChunksPlugin后Chrome扩展加载失败问题
问题描述
对基于 React 17.0.2 + Webpack 5.7 + Typescript 4.3.5 技术栈开发的 Chrome/Firefox 扩展做包体积优化时,配置了splitChunks拆分antd、moment为独立chunk,构建后可以正常生成对应拆分文件,但扩展加载后弹窗为空白页,检查DOM发现用于挂载React应用的root节点内容为空,多次排查未定位根因。
当前使用的webpack.common.js配置如下:
const webpack = require('webpack'); const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const manifestJson = require('./assets/manifest.json'); const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); const BrowserExtensionWebpackPlugin = require('./tools/browser_extension_webpack_plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const distDir = (function () { switch (process.env.BROWSER) { case 'chrome': return 'dist_crx'; case 'firefox': return 'dist_ff'; default: throw new Error('BROWSER not supported'); } })(); module.exports = { entry: { main: './src/global/popup.js', background: './src/global/bg/index.js', local_backup_page: './src/global/page-local-backup/local_backup.tsx', page_options: './src/global/page-options/options.js', payment_cs: './src/global/cs/cs_payment.js', payment_inject: './src/global/inject/inject_payment.js', updatecard_cs: './src/global/cs/cs_updatecard.js', updatecard_inject: './src/global/inject/inject_updatecard.js', }, output: { path: path.join(__dirname, distDir), filename: '[name].js', }, module: { rules: [ { test: /\.(js|jsx)?$/, exclude: /(node_modules)/, use: [ { loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react'], }, }, ], }, { test: /\.(ts|tsx)$/, use: 'ts-loader', exclude: /node_modules/, }, { test: /\.scss$/, use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'], }, { test: /\.css$/, use: ['style-loader', 'css-loader', 'postcss-loader'], }, ], }, resolve: { extensions: ['.tsx', '.ts', '.js', '.json'], alias: { ga: path.resolve( __dirname, process.env.BROWSER === 'firefox' ? './src/utils/lib/GoogleAnalytics/ga_mock.js' : './src/utils/lib/GoogleAnalytics/ga.js', ), '@': path.resolve(__dirname, 'src/'), }, fallback: { http: false, https: false, assert: false, tls: false, fs: false, net: false, }, }, plugins: [ new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }), new HtmlWebpackPlugin({ template: path.join(__dirname, 'assets', 'html', 'main.html'), filename: 'main.html', chunks: ['main'], }), new HtmlWebpackPlugin({ template: path.join(__dirname, 'assets', 'html', 'background.html'), filename: 'background.html', chunks: ['background'], }), new HtmlWebpackPlugin({ template: path.join(__dirname, 'assets', 'html', 'options.html'), filename: 'options.html', chunks: ['page_options'], }), new HtmlWebpackPlugin({ template: path.join(__dirname, 'assets', 'html', 'local_backup.html'), filename: 'local_backup.html', chunks: ['local_backup_page'], }), new NodePolyfillPlugin(), new CopyWebpackPlugin({ patterns: [ { from: 'assets', to: path.resolve(__dirname, distDir), }, ], }), new webpack.DefinePlugin({ __BROWSER__: JSON.stringify(process.env.BROWSER), __VERSION__: JSON.stringify(manifestJson.version), }), new BrowserExtensionWebpackPlugin({ createManifestJson: ({}) => { if (process.env.BROWSER === 'firefox') { delete manifestJson.content_security_policy; delete manifestJson.key; delete manifestJson.oauth2; delete manifestJson.options_page; delete manifestJson.background.persistent; manifestJson.permissions = manifestJson.permissions.filter( (str) => str !== 'identity.email', ); } else { delete manifestJson.applications; } return manifestJson; }, }), new BundleAnalyzerPlugin({ generateStatsFile: true, }), ], optimization: { splitChunks: { cacheGroups: { antd: { test: /[\\/]node_modules[\\/]antd[\\/]/, name: 'antd', chunks: 'all', }, moment: { test: /[\\/]node_modules[\\/]moment[\\/]/, name: 'moment', chunks: 'all', }, }, }, }, };
根因分析
白屏的直接原因是HtmlWebpackPlugin配置错误:每个页面的插件配置里硬编码了chunks参数,只传入了对应入口本身的chunk,没有包含splitChunks拆分出来的antd、moment等依赖chunk。
构建生成的HTML页面只会插入chunks字段里声明的JS文件,拆分出来的依赖文件不会被引入,入口JS运行时找不到对应依赖直接抛出引用错误,React初始化逻辑中断,无法挂载到root节点,最终表现为页面空白。
修复方案
- 优先方案:删除所有
HtmlWebpackPlugin配置项里的chunks字段,插件会自动识别每个入口依赖的所有拆分chunk,按依赖顺序插入对应script/link标签,不需要手动维护依赖关系,后续新增拆包规则也不用同步修改HTML配置,维护成本最低。 - 备选方案:如果需要保留
chunks配置做精确的资源加载控制,必须把当前入口依赖的所有拆分chunk都加入数组,例如popup页的配置需要改成chunks: ['antd', 'moment', 'main'],确保依赖资源先于入口文件加载。这种方式需要手动维护依赖关系,后续调整拆包规则时容易漏配,不推荐使用。
额外优化建议
- 已经把
moment拆为独立chunk的前提下,可以新增webpack.IgnorePlugin剔除moment默认打包的全量多语言包,能减少60%左右的moment包体积,配置如下:
new webpack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/ })
需要用到对应语言包时再手动引入即可。
- 当前样式处理统一使用
style-loader,会把样式内容注入到JS里,运行时插入内联style标签,容易触发浏览器扩展的CSP安全策略报错,可以把之前注释的MiniCssExtractPlugin启用,替换掉style-loader把样式抽成独立CSS文件,避免CSP拦截。
内容的提问来源于stack exchange,提问作者Gustavo Bermúdez
相关产品推荐
相关产品推荐

