webpack的CommonsChunkPlugin分析与优化

<p><code></p> <h2 id="articleHeader0">前言</h2> <p>在前端工程的的打包史中,common文件向来都不是一个好处理的方面。在这一块,webpack提供了CommonsChunkPlug<a href="http://www.js-code.com/tag/in" title="in" target="_blank">in</a>来处理这个事情,但是在由于文档的模棱两可,再加上各种配置选项的多样性和某些bug,还是有不少坑的。</p> <h2 id="articleHeader1">分析包</h2> <p>所谓工欲善其事必先利其器,我们既然想做common方面的优化,那么首先肯定要知道打包后的文件体积庞大的主要原因。说到这里就不得不提到一个相当好用的工具:<a href="https://github.com/th0r/webpack-bundle-analyzer" rel="nofollow noreferrer" target="_blank">webpack-bundle-analyzer</a></p> <p>它既是一个webpack插件,又是一个命令行工具。能够将webpack包的内容转换成可缩放的树状图,方便进行交互分析。恩。。。就是这玩意:<br /><span class="img-wrap"><img data-src="/img/remote/1460000008837589?w=908&amp;h=547" src="https://static.segmentfault.com/v-5cc2cd8e/global/img/squares.svg" alt="image" title="image" style="cursor: pointer;"></span></p> <h3 id="articleHeader2">安装</h3> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="npm install --save-dev webpack-bundle-analyzer" title="" data-original-title="复制"></span> </div> </p></div> <pre class="hljs sql"><code style="word-break: break-word; white-space: initial;">npm <span class="hljs-keyword">install</span> <span class="hljs-comment">--save-dev webpack-bundle-analyzer</span></code></pre> <h3 id="articleHeader3">作为插件使用</h3> <p>在 <code>webpack.config.js</code>中:</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; // ... plugins: [new BundleAnalyzerPlugin()] // ..." title="" data-original-title="复制"></span> </div> </p></div> <pre class="hljs typescript"><code><span class="hljs-keyword">var</span> BundleAnalyzerPlugin = <span class="hljs-built_in">require</span>(<span class="hljs-string">'webpack-bundle-analyzer'</span>).BundleAnalyzerPlugin; <span class="hljs-comment">// ...</span> plugins: [<span class="hljs-keyword">new</span> BundleAnalyzerPlugin()] <span class="hljs-comment">// ...</span></code></pre> <p>默认配置如下:</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="new BundleAnalyzerPlugin({ // Can be `server`, `static` or `disabled`. // In `server` mode analyzer will start HTTP server to show bundle report. // In `static` mode single HTML file with bundle report will be generated. // In `disabled` mode you can use this plugin to just generate Webpack Stats JSON file by setting `generateStatsFile` to `true`. analyzerMode: 'server', // Host that will be used in `server` mode to start HTTP server. analyzerHost: '127.0.0.1', // Port that will be used in `server` mode to start HTTP server. analyzerPort: 8888, // Path to bundle report file that will be generated in `static` mode. // Relative to bundles output directory. reportFilename: 'report.html', // Automatically open report in default browser openAnalyzer: true, // If `true`, Webpack Stats JSON file will be generated in bundles output directory generateStatsFile: false, // Name of Webpack Stats JSON file that will be generated if `generateStatsFile` is `true`. // Relative to bundles output directory. statsFilename: 'stats.json', // Options for `stats.toJson()` method. // For example you can exclude sources of your modules from stats file with `source: false` option. // See more options here: https://github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21 statsOptions: null, // Log level. Can be 'info', 'warn', 'error' or 'silent'. logLevel: 'info' })" title="" data-original-title="复制"></span> </div> </p></div> <pre class="hljs scala"><code><span class="hljs-keyword">new</span> <span class="hljs-type">BundleAnalyzerPlugin</span>({ <span class="hljs-comment">// Can be `server`, `static` or `disabled`.</span> <span class="hljs-comment">// In `server` mode analyzer will start HTTP server to show bundle report.</span> <span class="hljs-comment">// In `static` mode single HTML file with bundle report will be generated.</span> <span class="hljs-comment">// In `disabled` mode you can use this plugin to just generate Webpack Stats JSON file by setting `generateStatsFile` to `true`.</span> analyzerMode: <span class="hljs-symbol">'serve</span>r', <span class="hljs-comment">// Host that will be used in `server` mode to start HTTP server.</span> analyzerHost: <span class="hljs-symbol">'127</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-number">.1</span>', <span class="hljs-comment">// Port that will be used in `server` mode to start HTTP server.</span> analyzerPort: <span class="hljs-number">8888</span>, <span class="hljs-comment">// Path to bundle report file that will be generated in `static` mode.</span> <span class="hljs-comment">// Relative to bundles output directory.</span> reportFilename: <span class="hljs-symbol">'report</span>.html', <span class="hljs-comment">// Automatically open report in default browser</span> openAnalyzer: <span class="hljs-literal">true</span>, <span class="hljs-comment">// If `true`, Webpack Stats JSON file will be generated in bundles output directory</span> generateStatsFile: <span class="hljs-literal">false</span>, <span class="hljs-comment">// Name of Webpack Stats JSON file that will be generated if `generateStatsFile` is `true`.</span> <span class="hljs-comment">// Relative to bundles output directory.</span> statsFilename: <span class="hljs-symbol">'stats</span>.json', <span class="hljs-comment">// Options for `stats.toJson()` method.</span> <span class="hljs-comment">// For example you can exclude sources of your modules from stats file with `source: false` option.</span> <span class="hljs-comment">// See more options here: https://github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21</span> statsOptions: <span class="hljs-literal">null</span>, <span class="hljs-comment">// Log level. Can be 'info', 'warn', 'error' or 'silent'.</span> logLevel: <span class="hljs-symbol">'inf</span>o' })</code></pre> <h3 id="articleHeader4">命令行使用</h3> <p>第一步:</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="webpack --profile --json > stats.json" title="" data-original-title="复制"></span> </div> </p></div> <pre class="hljs sql"><code style="word-break: break-word; white-space: initial;">webpack <span class="hljs-comment">--profile --json &gt; stats.json</span></code></pre> <p>第二步:</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="webpack --profile --json | Out-file 'stats.json' -Encoding OEM" title="" data-original-title="复制"></span> </div> </p></div> <pre class="hljs fortran"><code style="word-break: break-word; white-space: initial;">webpack --profile --json | <span class="hljs-keyword">Out</span>-<span class="hljs-keyword">file</span> <span class="hljs-string">'stats.json'</span> -Encoding OEM</code></pre> <p>执行成功后,你将看到以下动态网页:</p> <p><span class="img-wrap"><img data-src="/img/bVMr8d?w=3675&amp;h=1840" src="https://static.segmentfault.com/v-5cc2cd8e/global/img/squares.svg" alt="图片描述" title="图片描述" style="cursor: pointer;"></span></p> <p>在这里顺便放上线上文件加载waterf<a href="http://www.js-code.com/tag/all" title="all" target="_blank">all</a>,作为对比。<br /><span class="img-wrap"><img data-src="/img/bVMzwX?w=1542&amp;h=1358" src="https://static.segmentfault.com/v-5cc2cd8e/global/img/squares.svg" alt="图片描述" title="图片描述" style="cursor: pointer;"></span></p> <h2 id="articleHeader5">问题</h2> <p>通过图表可以看到,在以下配置下:</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="config.plugins.push(new CommonsChunkPlugin('commons', 'js/commons.[hash].bundle.js'));" title="" data-original-title="复制"></span> </div> </p></div> <pre class="hljs scala"><code style="word-break: break-word; white-space: initial;">config.plugins.push(<span class="hljs-keyword">new</span> <span class="hljs-type">CommonsChunkPlugin</span>(<span class="hljs-symbol">'common</span>s', <span class="hljs-symbol">'js</span>/commons.[hash].bundle.js'));</code></pre> <p>打包出来的文件还是有很多问题的:</p> <ul> <li> <p>common包规划不合理, swiper.js ,<a href="http://www.js-code.com/tag/area" title="area" target="_blank">area</a>.json等公用文件大量重复加载。</p> </li> <li> <p>antd 没有抽离出来,无法并行加载,也无法进一步做运行时按需加载。</p> </li> <li> <p>e<a href="http://www.js-code.com/tag/char" title="char" target="_blank">char</a>ts在每个使用的包都单独打包一份,只要包含e<a href="http://www.js-code.com/tag/char" title="char" target="_blank">char</a>ts的包,基本一百多kb。(线上压缩并开启gzip)</p> </li> <li> <p><code><a href="http://www.js-code.com/tag/import" title="import" target="_blank">import</a> {ImgBigSwiper} from 'components/src/<a href="http://www.js-code.com/tag/in" title="in" target="_blank">in</a>dex'; </code> 这种写法会导致将components里面的所有组件全部打包进页面的js。应该这样写:<code><a href="http://www.js-code.com/tag/import" title="import" target="_blank">import</a> ImgBigSwiper from 'components/src/ImgBigSwiper';</code>挨个引入,见<a href="https://segmentfault.com/q/1010000009110170">webpack将ES6编译成CommonJs后只引入用到的模块</a></p> </li> <li> <p>common.js独占490kb,要等这个包加载完后index才开始解析路由。</p> </li> </ul> <p>在这个过程中,会发现一个有趣的事情。就是index.html页面的script加载分为以下两个部分:</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="<a href="http://www.js-code.com/tag/button" title="button" target="_blank">button</a>" <a href="http://www.js-code.com/tag/class" title="class" target="_blank">class</a>="copyCode code-tool" data-toggle="tooltip" data-placement="<a href="http://www.js-code.com/tag/top" title="top" target="_blank">top</a>" data-clipboard-<a href="http://www.js-code.com/tag/text" title="text" target="_blank">text</a>="......<br /> <script type=&quot;text/javascript&quot; src=&quot;//res.dinghuo123.com/src/common/ueditor/ueditor.config.js&quot;></script><br /> <script type=&quot;text/javascript&quot; src=&quot;//res.dinghuo123.com/src/common/ueditor/ueditor.config.js&quot;></script><br /> <script type=&quot;text/javascript&quot; src=&quot;//res.dinghuo123.com/src/common/ueditor/ueditor.config.js&quot;></script><br /> ......<br /> <script> <a href="http://www.js-code.com/tag/do" title="do" target="_blank">do</a>cument.write('<script src=&quot;https://resource.dinghuo123.com/dist/ydhv2/webpack.assets.js?v=' + Math.random() + '&quot;></script>');<br /> <a href="http://www.js-code.com/tag/document" title="document" target="_blank">document</a>.write('<script src=&quot;' + window.WEBPACK_ASSETS['commons'].js + '&quot;></script>');<br /> <a href="http://www.js-code.com/tag/do" title="do" target="_blank">do</a>cument.write('<script src=&quot;//res.dinghuo123.com/src/common/ueditor/ueditor.config.js&quot;></script>');<br /> <a href="http://www.js-code.com/tag/document" title="document" target="_blank">document</a>.write('<script src=&quot;' + window.WEBPACK_ASSETS['index'].js + '&quot;></script>');<br /> </script>" title="" data-original-title="复制"></span> </div> </p></div> <pre class="hljs xml"><code>...... <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text/javascript"</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"//res.dinghuo123.com/src/common/ueditor/ueditor.config.js"</span>&gt;</span><span class="undefined"></span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text/javascript"</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"//res.dinghuo123.com/src/common/ueditor/ueditor.config.js"</span>&gt;</span><span class="undefined"></span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text/javascript"</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"//res.dinghuo123.com/src/common/ueditor/ueditor.config.js"</span>&gt;</span><span class="undefined"></span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span> ...... <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript"> <span class="hljs-built_in">document</span>.write(<span class="hljs-string">'&lt;script src="https://resource.dinghuo123.com/dist/ydhv2/webpack.assets.js?v='</span> + <span class="hljs-built_in">Math</span>.random() + <span class="hljs-string">'"&gt;&lt;/script&gt;'</span>); <span class="hljs-built_in">document</span>.write(<span class="hljs-string">'&lt;script src="'</span> + <span class="hljs-built_in">window</span>.WEBPACK_ASSETS[<span class="hljs-string">'commons'</span>].js + <span class="hljs-string">'"&gt;&lt;/script&gt;'</span>); <span class="hljs-built_in">document</span>.write(<span class="hljs-string">'&lt;script src="//res.dinghuo123.com/src/common/ueditor/ueditor.config.js"&gt;&lt;/script&gt;'</span>); <span class="hljs-built_in">document</span>.write(<span class="hljs-string">'&lt;script src="'</span> + <span class="hljs-built_in">window</span>.WEBPACK_ASSETS[<span class="hljs-string">'index'</span>].js + <span class="hljs-string">'"&gt;&lt;/script&gt;'</span>); </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></code></pre> <p>然后你会发现,是上面一块的script并行加载完,才并行加载下一个script标签的内容。大家可以思考一下为什么。</p> <h2 id="articleHeader6">改进配置</h2> <p>进过调整之后的CommonsChunkPlugin配置:</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="config.plugins.push(new CommonsChunkPlugin({ name: 'commons', minChunks: Infinity // 随着 入口chunk 越来越多,这个配置保证没其它的模块会打包进 公共chunk })); config.plugins.push(new CommonsChunkPlugin({ async:'antd', minChunks(module) { var context = module.context; return context &amp;&amp; context.indexOf('antd/dist') >= 0;<br /> }<br /> }));</p> <p>config.plugins.push(new CommonsChunkPlugin({<br /> async:'echarts',<br /> minChunks(module) {<br /> var context = module.context;<br /> return context &amp;&amp; (context.indexOf('echarts') >= 0 || context.indexOf('zrender') >= 0);<br /> }<br /> }));" title="" data-original-title="复制"></span> </div> </p></div> <pre class="hljs typescript"><code>config.plugins.push(<span class="hljs-keyword">new</span> CommonsChunkPlugin({ name: <span class="hljs-string">'commons'</span>, minChunks: <span class="hljs-literal">Infinity</span> <span class="hljs-comment">// 随着 入口chunk 越来越多,这个配置保证没其它的模块会打包进 公共chunk</span> })); config.plugins.push(<span class="hljs-keyword">new</span> CommonsChunkPlugin({ <span class="hljs-keyword">async</span>:<span class="hljs-string">'antd'</span>, minChunks(<span class="hljs-keyword">module</span>) { <span class="hljs-keyword">var</span> context = <span class="hljs-built_in">module</span>.context; <span class="hljs-keyword">return</span> context &amp;&amp; context.indexOf(<span class="hljs-string">'antd/dist'</span>) &gt;= <span class="hljs-number">0</span>; } })); config.plugins.push(<span class="hljs-keyword">new</span> CommonsChunkPlugin({ <span class="hljs-keyword">async</span>:<span class="hljs-string">'echarts'</span>, minChunks(<span class="hljs-keyword">module</span>) { <span class="hljs-keyword">var</span> context = <span class="hljs-built_in">module</span>.context; <span class="hljs-keyword">return</span> context &amp;&amp; (context.indexOf(<span class="hljs-string">'echarts'</span>) &gt;= <span class="hljs-number">0</span> || context.indexOf(<span class="hljs-string">'zrender'</span>) &gt;= <span class="hljs-number">0</span>); } }));</code></pre> <p>这里用到了minChunks和async两个配置。</p> <h3 id="articleHeader7">minChunks</h3> <p>其中第一<a href="http://www.js-code.com/tag/name" title="name" target="_blank">name</a>的<code>commons</code>是一个<code>en<a href="http://www.js-code.com/tag/try" title="try" target="_blank">try</a></code>入口,里面是一个依赖包的<a href="http://www.js-code.com/tag/%e6%95%b0%e7%bb%84" title="数组" target="_blank">数组</a>。<code>minChunks</code>设置为<code><a href="http://www.js-code.com/tag/Infinity" title="Infinity" target="_blank">Infinity</a></code>这个配置保证没其它的模块会打包进 公共chunk。因为说实话,CommonsChunkPlugin的commons分析实在是不怎么只能,还是手动控制会更好一些。<br />当然,你可以传入一个 <code><a href="http://www.js-code.com/tag/function" title="function" target="_blank">function</a></code> ,以添加定制的逻辑(默认是 chunk 的数量),这个函数会被 CommonsChunkPlugin 插件回调,并且调用函数时会传入 module 和 count 参数。<br />module 参数代表每个 chunks 里的模块,这些 chunks 是你通过 <a href="http://www.js-code.com/tag/name" title="name" target="_blank">name</a>/names 参数传入的。</p> <ul> <li> <p>module.con<a href="http://www.js-code.com/tag/text" title="text" target="_blank">text</a>: The directory that stores the file. For example: '/my_project/<a href="http://www.js-code.com/tag/node" title="node" target="_blank">node</a>_modules/example-dependency'</p> </li> <li> <p>module.resource: The name of the file being processed. For example: '/my_project/<a href="http://www.js-code.com/tag/node" title="node" target="_blank">node</a>_modules/example-dependency/index.js'</p> </li> <li> <p>count 参数表示 module 被使用的 chunk 数量。</p> </li> </ul> <p>当你想要对 CommonsChunk 如何决定模块被打包到哪里的算法有更为细致的控制, 这个配置就会非常有用。</p> <h3 id="articleHeader8">async</h3> <p>下面的内容是官网弄过来的,其实我也看不太懂。。。</p> <blockquote> <p>如果设置为 <code><a href="http://www.js-code.com/tag/true" title="true" target="_blank">true</a></code>,一个异步的 公共chunk 会作为 <code><a href="http://www.js-code.com/tag/option" title="option" target="_blank">option</a>s.name</code> 的子模块,和 <code><a href="http://www.js-code.com/tag/option" title="option" target="_blank">option</a>s.chunks</code> 的兄弟模块被创建。它会与 <code>options.chunks</code> 并行被加载。可以通过提供想要的字符串,而不是 <code><a href="http://www.js-code.com/tag/true" title="true" target="_blank">true</a></code> 来对输出的文件进行更换名称。</p> </blockquote> <h2 id="articleHeader9">结果</h2> <p>还是先看打包分析的结果吧:<br /><span class="img-wrap"><img data-src="/img/bVMzx8?w=3676&amp;h=1846" src="https://static.segmentfault.com/v-5cc2cd8e/global/img/squares.svg" alt="图片描述" title="图片描述" style="cursor: pointer;"></span></p> <p>通过上面分析可以看到:</p> <ol> <li> <p>common合理划分,抓大放小。</p> </li> <li> <p>antd和echarts提取出来,并行加载。</p> </li> <li> <p>components 用到什么打包什么。(手动控制的)</p> </li> <li> <p>大大减小了其他业务包的体积,93%的业务包大小控制在25K以内,剩下7%的业务包大小控制在50k以内。(开启gzip)</p> </li> <li> <p>首屏加载资源的总大小几乎没有变化。</p> </li> </ol> <h2 id="articleHeader10">接下来的方向</h2> <ol> <li> <p>echarts 和Ueditor运行时按需加载。</p> </li> <li> <p><code>tree-shaking</code>的探索</p> </li> </ol> <h2 id="articleHeader11">参考</h2> <p><a href="https://medium.com/@adamrackis/vendor-and-code-splitting-in-webpack-2-6376358f1923" rel="nofollow noreferrer" target="_blank">Vendor and code splitting in webpack 2</a><br /><a href="https://github.com/eyasliu/blog/issues/8" rel="nofollow noreferrer" target="_blank">webpack 按需打包加载</a><br /><a href="http://webpack.github.io/docs/code-splitting.html#split-app-and-vendor-code" rel="nofollow noreferrer" target="_blank">weboack Split app and vendor code</a><br /><a href="https://github.com/webpack-china/awesome-webpack-cn" rel="nofollow noreferrer" target="_blank">awesome-webpack-cn</a></p> <p></code></p>
脚本宝典为你提供优质服务
脚本宝典 » webpack的CommonsChunkPlugin分析与优化

发表评论

提供最优质的资源集合

立即查看 了解详情