webpack—性能优化

211 阅读7分钟

优化打包构建速度(开发环境性能用户是哪个)

HMR

HMR: hot module replacement 热模块替换 / 模块热替换 作用:一个模块发生变化,只会重新打包这一个模块(而不是打包所有模块) 极大提升构建速度

  • 样式文件:可以使用HMR功能:因为style-loader内部实现了~(MiniCssExtractPlugin据说也实现了,等会测试一下)
  • js文件:默认不能使用HMR功能 --> 需要修改js代码,添加支持HMR功能的代码 注意:HMR功能对js的处理,只能处理非入口js文件的其他文件。
if (module.hot) {
  // 一旦 module.hot 为true,说明开启了HMR功能。 --> 让HMR功能代码生效
  module.hot.accept('./print.js', function() {
    // 方法会监听 print.js 文件的变化,一旦发生变化,其他模块不会重新打包构建。
    // 会执行后面的回调函数
    print();
  });
}
  • html文件: 默认不能使用HMR功能.同时会导致问题:html文件不能热更新了~ (不用做HMR功能) 解决:修改entry入口,将html文件引入

优化代码调试 source-map

source-map: 一种 提供源代码到构建后代码映射 技术 (如果构建后代码出错了,通过映射可以追踪源代码错误)

module.exports = {
    devtool: 'eval-source-map'
}
常用有:[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
内联 和 外部的区别:1. 外部生成了文件,内联没有 2. 内联构建速度更快
  • source-map:外部 错误代码准确信息 和 源代码的错误位置
  • inline-source-map:内联 只生成一个内联source-map 错误代码准确信息 和 源代码的错误位置
  • hidden-source-map:外部 错误代码错误原因,但是没有错误位置 不能追踪源代码错误,只能提示到构建后代码的错误位置
  • eval-source-map:内联 每一个文件都生成对应的source-map,都在eval 错误代码准确信息 和 源代码的错误位置
  • nosources-source-map:外部 错误代码准确信息, 但是没有任何源代码和构建代码的信息
  • cheap-source-map:外部 错误代码准确信息 和 源代码的错误位置 只能精确的行
  • cheap-module-source-map:外部 错误代码准确信息 和 源代码的错误位置 module会将loader的source map加入
开发环境:速度快,调试更友好
  • 速度快(eval>inline>cheap>...)
    • eval-cheap-souce-map
    • eval-source-map
  • 调试更友好
    • souce-map
    • cheap-module-souce-map
    • cheap-souce-map

综上所述,开发环境选择: eval-source-map / eval-cheap-module-souce-map

生产环境

源代码要不要隐藏? 调试要不要更友好。内联会让代码体积变大,所以在生产环境不用内联

  • nosources-source-map 全部隐藏
  • hidden-source-map 只隐藏源代码,会提示构建后代码错误信息

oneOf

优化点: 每个不同类型的文件在loader转换时,都会被命中,遍历module中rules中所有loader

babel缓存

  • cacheDirectory: true 让第二次打包构建速度更快
{
    test: /\.js$/,
    exclude: /node_modules/,
    loader: 'babel-loader',
    options: {
      presets: [
        [
          '@babel/preset-env',
          {
            useBuiltIns: 'usage',
            corejs: { version: 3 },
            targets: {
              chrome: '60',
              firefox: '50'
            }
          }
        ]
      ],
      // 开启babel缓存
      // 第二次构建时,会读取之前的缓存
      cacheDirectory: true
    }
}
  • 文件资源缓存
    • hash: 每次wepack构建时会生成一个唯一的hash值。

      问题: 因为js和css同时使用一个hash值。如果重新打包,会导致所有缓存失效。(可能我却只改动一个文件)

    • chunkhash:根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样(入口文件是一个,就是一个chunk)

      问题: js和css的hash值还是一样的,因为css是在js中被引入的,所以同属于一个chunk

    • contenthash: 根据文件的内容生成hash值。不同文件hash值一定不一样,让代码上线运行缓存更好使用

多进程打包 thread-loader

module.exports = {
    module: {
        rules: [
            {
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
              /* 
                开启多进程打包。 
                进程启动大概为600ms,进程通信也有开销。
                只有工作消耗时间比较长,才需要多进程打包
              */
              {
                loader: 'thread-loader',
                options: {
                  workers: 2 // 进程2个
                }
              },
              {
                loader: 'babel-loader'
              }
            ]
          }
        ]
    }
}

externals

拒绝第三方库被打包进来,通过cdn引入第三方库

譬如jquery

  1. 在html中引入
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>

2、在webpack.config.js中配置

module.exports = {
  externals: {
    // 拒绝jQuery被打包进来
    jquery: 'jQuery'
  }
};

dll

使用dll技术,对某些库(第三方库:jquery、react、vue...)进行单独打包

单独给第三方库配置webpack文件(webpack.dll.js),启动打包成库

const { resolve } = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    // 最终打包生成的[name] --> jquery
    // ['jquery'] --> 要打包的库是jquery
    jquery: ['jquery'],
  },
  output: {
    filename: '[name].js',
    path: resolve(__dirname, 'dll'),
    library: '[name]_[hash]' // 打包的库里面向外暴露出去的内容叫什么名字
  },
  plugins: [
    // 打包生成一个 manifest.json --> 提供和jquery映射
    new webpack.DllPlugin({
      name: '[name]_[hash]', // 映射库的暴露的内容名称,与上面的library映射
      path: resolve(__dirname, 'dll/manifest.json') // 输出文件路径
    })
  ],
  mode: 'production'
};

webpack.config.js文件

const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'built.js',
    path: resolve(__dirname, 'build')
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
    // 告诉webpack哪些库不参与打包,同时使用时的名称也得变~
    new webpack.DllReferencePlugin({
      manifest: resolve(__dirname, 'dll/manifest.json')
    }),
    // 将某个文件打包输出去,并在html中自动引入该资源
    new AddAssetHtmlWebpackPlugin({
      filepath: resolve(__dirname, 'dll/jquery.js')
    })
  ],
  mode: 'production'
};

优化代码运行的性能

tree shaking

参考文献 —— Webpack Tree shaking 深入探究

tree shaking:去除无用代码,减少代码体积。 因为ES6模块的出现,ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是Tree shaking的基础。所以tree shaking是必须使用es6模块化的 注意事项:

1. 开启production环境

 在webpack配置文件中设置mode: 'production',
 

2. 在package.json中配置,设置 sideEffects

    1. "sideEffects": false 所有代码都没有副作用(都可以进行tree shaking)
        问题:可能会把css / @babel/polyfill (副作用)文件干掉
    2. 所以我们可以使用:"sideEffects": ["*.css", "*.less"]

3. 在webpack-config.js中

    optimization: {
        minimize: false // 这个不能设置为false,否则树摇无效
    }

4. 使用export.defaut需要注意

   - 导入情况1
        text.js文件:
        export default function mul(x, y) {
          console.log('sssssss');
          return x * y;
        }

        function count(x, y) {
          return x - y;
        }
        index.js文件:
        import mul from './test';
        console.log(mul(2, 3));
   这样使用export.default导入文件是有效果的,count不会打包进去压缩文件
   
   - 导出情况2 
       text.js文件:
        function mul(x, y) {
          console.log('sssssss');
          return x * y;
        }

        function count(x, y) {
          return x - y;
        }
        export {mul, count};
        index.js文件:
        import test from 'test';
        console.log(test.mul(2, 3));
   这样使用export.default导入文件是没有效果的,count还是会打包进去压缩文件

Webpack Tree shaking不会清除IIFE(立即调用函数表达式)

export function mul(x, y) {
    console.log('sss');
    return x * y;
}
const square = (function (x) {
    console.log('square');
    return x*x;
}());
export function count(x, y) {
    console.log('kkkkk');
    return x - y;
}
立即执行函数是不会去掉的, 但是Webpack Tree shaking对于IIFE的返回结果,如果未使用会被清除,也就是
只会保留console.log('square');
其他都会被去除

原因:因为IIFE比较特殊,它在被翻译时(JS并非编译型的语言)就会被执行,Webpack不做程序流分析,它不知道IIFE会做什么特别的事情,所以不会删除这部分代码

Babel 为何会导致 Tree Shaking 失败

是否成功取决于你的 babel 版本,简单的说,如果你使用的是 babel 6,那 Tree Shaking 不生效,如果使用了 Babel 7 及其以上版本,Tree Shaking 是生效的。

我们已经清楚,CommonJS模块和ES6的模块是不一样的,Babel在处理时默认将所有的模块转换成为了exports结合require的形式,我们也清楚Webpack是基于ES6的模块才能做到最大程度的Tree shaking的,所以我们在使用Babel时,应该将Babel的这一行为关闭,方式如下:

//babel.rc
presets: [["env", 
  { module: false }
]]

如果是es7,引入loash,需要这样引入

import { pullAll } from 'lodash-es';
import pullAll from 'lodash/pullAll';

code split

  • 多入口
module.exports = {
  // 单入口
  // entry: './src/js/index.js',
  entry: {
    // 多入口:有一个入口,最终输出就有一个bundle
    index: './src/js/index.js',
    test: './src/js/test.js'
  }
}
  • 设置
module.exports = {
  /*
    1. 可以将node_modules中代码单独打包一个chunk最终输出
    2. 自动分析多入口chunk中,有没有公共的文件。如果有会打包成单独一个chunk
  */
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  },
  mode: 'production'
};
  • import
/*
  通过js代码,让某个文件被单独打包成一个chunk
  import动态导入语法:能将某个文件单独打包
*/
import(/* webpackChunkName: 'test' */'./test')
  .then(({ mul, count }) => {
    // 文件加载成功~
    // eslint-disable-next-line
    console.log(mul(2, 5));
  })
  .catch(() => {
    // eslint-disable-next-line
    console.log('文件加载失败~');
  });

懒加载/预加载

document.getElementById('btn').onclick = function() {
  // 懒加载~:当文件需要使用时才加载~
  // 预加载 prefetch:会在使用之前,提前加载js文件 
  // 正常加载可以认为是并行加载(同一时间加载多个文件)  
  // 预加载 prefetch:等其他资源加载完毕,浏览器空闲了,再偷偷加载资源
  import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test').then(({ mul }) => {
    console.log(mul(4, 5));
  });
};

pwa 渐进式网络开发应用程序(离线可访问)

PWA: 渐进式网络开发应用程序(离线可访问)

webpack配置文件中

plugins: [
    new WorkboxWebpackPlugin.GenerateSW({
      /*
        1. 帮助serviceworker快速启动
        2. 删除旧的 serviceworker

        生成一个 serviceworker 配置文件~
      */
      clientsClaim: true,
      skipWaiting: true
    })
]

js入口文件中

/*
  1. eslint不认识 window、navigator全局变量
    解决:需要修改package.json中eslintConfig配置
      "env": {
        "browser": true // 支持浏览器端全局变量
      }
   2. sw代码必须运行在服务器上
      --> nodejs
      -->
        npm i serve -g
        serve -s build 启动服务器,将build目录下所有资源作为静态资源暴露出去
*/
// 注册serviceWorker
// 处理兼容性问题
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/service-worker.js')
      .then(() => {
        console.log('sw注册成功了~');
      })
      .catch(() => {
        console.log('sw注册失败了~');
      });
  });
}