webpack常见的优化手段汇总

treeshaking, scope hoisting等

Posted by Cycle263 on March 9, 2019

webpack之性能优化

webpack的优化技巧,提升构建速度(并行编译、hash缓存、预编译),减少构建包大小,加快页面加载速度(减少编译体积-treeShaking scopeHositing、hash缓存、分包机制)等。
  • 优化的相关概念

    • 构建时间:一次全量构建 = install modules time + webpack loaders time + webpack plugins time
  • 具体优化细节

    • install优化,一般可使用国内镜像,例如:cnpm,tnpm 等。镜像详情参见

      这里推荐使用npminstall工具,安装依赖速度提升明显,提升幅度参见

    • 依赖包大小优化

      • 去掉重复的依赖包,删除未使用依赖包,uninstall移除的依赖包;

      • 精准尽可能小的依赖包,或者使用小功能包替换大而全的功能包;fecha -> moment, lodash/isequal || lodash.isequal -> lodash,缺点是可能引发重复打包。在使用antd 的时间组件时,不建议替换moment,因为重度依赖moment。

    • 另外,使用第三方库的优化插件,例如:babel-plugin-antd、babel-plugin-lodash等

      // 使用时,插件会进行组件关联性梳理,只引入相关组件和样式
      webpackConfig.babel.plugins.push(['antd', {
        style: 'css',
      }]);
      
    • babel优化,polyfill使用方式优化,推荐使用presets引入方式,不建议使用require('babel-pollyfill');方式,babel polyfill引入方式详情

    • css-module,增加样式hash后缀,避免组件之间的样式干扰,缺点是增大了打包后的文件大小。

    • jsx文件未ES5化,成熟的 npm 包会在发布前将自己 es5,甚至 es3 化,这些依赖包完全没有经过 babel 的必要。可以配置loader的exclude项过滤。实际上,在当下 2018 年,对于大部分用户(90%)而言,我们根本不需要把代码编译到 ES5,不仅体积大,而且运行速度慢。支持 <script type="module"> 的浏览器,必然支持下面的特性:async/await、Promise、Class、箭头函数、Map/Set、fetch 等等…

    而不支持 <script type="module"> 的老旧浏览器,会因为无法识别这个标签,而不去加载 ES2015+ 的代码。另外老旧的浏览器同样无法识别 nomodule 熟悉,会自动忽略它,从而加载 ES5 标准的代码。

      {
        test: /\.js(x)*$/,
        loader: 'babel-loader',
        exclude: function(path) {
            // 路径中含有 node_modules 的就不去解析。
            var isNpmModule = !!path.match(node_modules/);
            return isNpmModule;
        },
        query: {
            presets: ['react', 'es2015-ie', 'stage-1']
        }
      }
    
    • externals,把我们的依赖申明为一个外部依赖,外部依赖通过 <script> 外链脚本引入。这样配置可以减少打包构建速度,充分利用CDN缓存机制,具体配置: externals: ['react', 'react-dom', 'react-router']

    • alias, resolve.alias 模块别名定义,直接引用别名即可,也可以提高webpack搜索的速度

    • noParse,module.noParse会让 webpack 忽略对其进行文件的解析,直接会进入最后的 bundle。例如:react.min.js这类没有依赖的模块,构建速度会更快。

    • DllPlugin 和 DllReferencePlugin, deps 中也引用了大量的 npm 包,而这些包在正常的开发过程中并不会进行修改,但是在每一次构建过程中却需要反复的将其分析,使用dllplugin可以避免这样的消耗。

    简单来说 DllPlugin 的作用是预先编译一些模块,而 DllReferencePlugin 则是把这些预先编译好的模块引用起来。这边需要注意的是 DllPlugin 必须要在 DllReferencePlugin 执行前,执行过一次。它的缺点也很明显,就是很多重复的内容被多次打包进了bundle文件,因此不适合生产环境使用。

    dllPlugin 和 commonChunkPlugin 是二选一的,并且在启用 dll 后和 external、common 一样需要在页面中引用对应的脚本,在 dll 中就是需要手动引用 vendor.dll.js。

    • stats (string/object)打包过程的日志信息展示,五种输出级别:”errors-only”、”minimal”、”none”、”normal”、”verbose”,stats详细介绍

    • 懒加载,也即是按需加载,require.ensure 或者 bundle-loader,syntax-dynamic-import,react-loadable

    require.ensure 内部依赖于 Promises,旧的浏览器中使用记得引入 es6-promise polyfill。

    React Loadable 将动态引入(dynamic import)封装成了一个对 React 友好的 API 来在特定组件下引入代码分割的功能。

      // bundle-loader
      bundle((file) => {...})
    
      require.ensure('module', () => {...})
    
      // babel-plugin-syntax-dynamic-import
      import('module').then(() => {...}).catch()
    
      // react-loadable高阶组件方式
      const LoadableBar = Loadable({
        loader: () => import('./components/Bar'),
        loading() {
          return <div>Loading...</div>
        }
      });
    
    • 图片懒加载

      react-lazyload、react-lazy-load。也可以先加载一张低像素的模糊图片,然后等高清图片加载完毕进行替换。lazyload 组件的原理大概分为两种:

      • 监听 window 对象或者父级对象的 scroll 事件,触发 load;

      • 使用 Intersection Observer API 来获取元素的可见性。

    • 图片加载优化

      base64化,减少http请求,推荐使用HTTP2,多路连接复用

      使用压缩的图片格式webP、sharpP(压缩图方案,比webP效率更高)

    • 占位显示(placeholder)

      在加载文本、图片的时候,经常出现“闪屏”的情况,比如图片或者文字还没有加载完毕,此时页面上对应的位置还是完全空着的,然后加载完毕,内容会突然撑开页面,导致“闪屏”的出现,造成不好的体验。为了避免这种突然撑开的情况,我们要做的就是提前设置占位元素,也就是 placeholder,成熟的第三方组件:react-placeholder、react-hold

    • 作用域提升 - scope hoisting

      ModuleConcatenationPlugin(webpack3) -> optimization.concatenateModules(webpack4)。scope hoisting的效果同样也依赖于静态分析。带来的好处是,减少bundle体积,较少不必要的作用域。

      实现原理:分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。因此,只有被引用过一次的模块才会被合并。

      失效情况: 必须采用ES6模块化语法的代码,因为ES6模块化的代码是静态的,webpack才能分析出需要剔除哪些代码。

      /* 开启前 */
      [
        /* 0 */
        function(module, exports, require) {
            var module_a = require(1)
            console.log(module_a['default'])
        }
      
        /* 1 */
        function(module, exports, require) {
            exports['default'] = 'module A'
        }
      ]
      
      /* 开启后 */
      [
        function(module, exports, require) {
            var module_a_defaultExport = 'module A'
            console.log(module_a_defaultExport)
        }
      ]
      
    • tree Shaking

      Tree shaking一直是一个美丽而遥不可及的话题,它是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码。这个术语和概念实际上是兴起于 ES2015 模块打包工具 rollup。可以简单地理解为摇树,抖落掉枯萎无用的树叶。

    • HappyPack

      webpack的打包过程是io密集和计算密集型的操作,如果能同时fork多个进程并行处理各个任务,将会有效的缩短构建时间,HappyPack就能做到这点。其中happyThreadPool是根据cpu数量生成的共享进程池,防止过多的占用系统资源。

      const HappyPack = require('happypack');
      const os = require('os');
      
      const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
      
      module.exports = {
        ...
        module: {
          plugins: [
            ...
            new HappyPack({
                id: 'happyBabel',
                loaders: [{
                    loader: 'babel-loader',
                    options: {
                        cacheDirectory: true,
                        presets: ['react', 'es2015', 'stage-0'],
                        plugins: ['add-module-exports', 'transform-decorators-legacy'],
                    },
                }],
                threadPool: happyThreadPool,
                verbose: true,
            }),
            new HappyPack({
                id: 'happyCss',
                loaders: ['css-loader', 'postcss-loader'],
                threadPool: happyThreadPool,
                verbose: true,
            }),
          ],
        }
      };
      

分割代码的方式

  • 入口方式:使用 entry 配置手动地分离代码。

    • 入口chunks之间包含重复的模块(lodash),那些重复模块(lodash)都会被引入到各个bundle中。

    • 这种方式不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码。

  • CommonsChunkPlugin:使用 CommonsChunkPlugin 去重和分离 chunk。

    • name和names:chunk的名称,如果这个chunk已经在entry中定义,该chunk会被直接提取;如果没有定义,则生成一个空的chunk来提取其他所有chunk的公共代码。
  • 动态import:通过模块的内联函数调用来分离代码。

  • 按需加载

基于路由的按需加载