第 4 章 webpack 性能优化

对于开发环境而言,我们主要优化打包构建速度优化代码调试

开发环境性能优化

对于生产环境而言,我们主要优化打包构建速度优化代码运行的性能

生产环境性能优化

HMR

HMR(Hot Module Replacement)是一种前端开发中常用的技术,用于在开发过程中实现模块的热替换,从而实现实时预览和快速开发的效果。HMR 可以使开发者在不刷新整个页面的情况下,实时更新修改的模块,以提高开发效率。

HMR 的基本原理是在应用程序运行过程中,将新的模块代码替换掉旧的模块代码,同时保持应用程序的状态和数据。通过使用热更新中间件和热模块替换插件,开发者可以在代码发生变化时,将新的模块代码注入到运行中的应用程序中,从而实现模块级别的热替换。

对于样式文件,当我们使用style-loader时,我们可以直接使用HMR功能。对于html文件,因为只有一个,所以默认我们不使用HMR功能。HMR 主要用于处理 JavaScript 模块的热更新。

开启HMR默认功能时,我们只需配置devServer就可以了。

devServer: {
    contentBase: resolve(__dirname, 'build'),
    compress: true,
    port: 3000,
    open: true,
    hot: true
  }

但是这个时候,一旦有任意模块中的js代码发生变化都会重新加载。为了只加载发生变化的模块,我们在入口文件针对其他子模块做判断

if (module.hot) {
  // 一旦 module.hot 为true,说明开启了HMR功能。 --> 让HMR功能代码生效
  module.hot.accept()
}

module.hot.accept() 是 HMR 的 API,用于接受新模块的更新。当某个模块发生变化时,会触发 HMR,然后执行 module.hot.accept() 来接受更新,并将新的模块替换掉旧的模块。

但是生产环境不需要开启热模块,所以针对以上代码我们要稍作修改。

if (process.env.NODE_ENV !== 'production') { 
    if (module.hot) { 
        module.hot.accept(); 
    } 
}

source-map

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

  • 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加入

内联 和 外部的区别:1. 外部生成了文件,内联没有 2. 内联构建速度更快

按打包速度排序:

  1. eval-souce-map
  2. inline-souce-map
  3. cheap-souce-map

按调试友好度:

  1. souce-map
  2. cheap-module-souce-map
  3. cheap-souce-map

对于开发环境我们追求速度快,调试更友好,我们建议: eval-source-map(react和vue的脚手架的选择)

对于生产环境我们要考虑构建包的体积大小,源代码要不要隐藏,调试要不要更友好?

  • 内联会让代码体积变大,所以在生产环境不用内联
  • 如果要全部隐藏,nosources-source-map
  • 只隐藏源代码,会提示构建后代码错误信息,hidden-source-map
  • 调试友好,source-map
  • 速度快一点:cheap-module-souce-map

最终我们建议使用:source-map

oneOf

oneOfrules 数组的一个特殊配置项,用于处理特定规则的加载器。它允许我们对不同类型的文件应用不同的加载器,并且只使用第一个匹配的规则,而不会继续向下应用其他规则。

换句话说,rules中对应的每个loader的test正则匹配会去读取所有文件,而用oneOf包裹之后,之前匹配到的文件类型不再进行重复匹配。

 rules: [
      {
        // 在package.json中eslintConfig --> airbnb
        test: /\.js$/,
        exclude: /node_modules/,
        // 优先执行
        enforce: 'pre',
        loader: 'eslint-loader',
        options: {
          fix: true
        }
      },
      {
        // 以下loader只会匹配一个
        // 注意:不能有两个配置处理同一种类型文件
        oneOf: [
          {
            test: /\.css$/,
            use: [...commonCssLoader]
          },
          {
            test: /\.less$/,
            use: [...commonCssLoader, 'less-loader']
          },
          /*
            正常来讲,一个文件只能被一个loader处理。
            当一个文件要被多个loader处理,那么一定要指定loader执行的先后顺序:
              先执行eslint 在执行babel
          */
          {
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
            options: {
              presets: [
                [
                  '@babel/preset-env',
                  {
                    useBuiltIns: 'usage',
                    corejs: {version: 3},
                    targets: {
                      chrome: '60',
                      firefox: '50'
                    }
                  }
                ]
              ]
            }
          },
          {
            test: /\.(jpg|png|gif)/,
            loader: 'url-loader',
            options: {
              limit: 8 * 1024,
              name: '[hash:10].[ext]',
              outputPath: 'imgs',
              esModule: false
            }
          },
          {
            test: /\.html$/,
            loader: 'html-loader'
          },
          {
            exclude: /\.(js|css|less|html|jpg|png|gif)/,
            loader: 'file-loader',
            options: {
              outputPath: 'media'
            }
          }
        ]
      }
    ]

缓存

Babel 缓存

在babel-loader中配置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值来命名,当名称发生变化时,浏览器会重新从服务器加载资源。这样做是让代码上线运行缓存更好使用。hash有以下3种。

hash

每次wepack构建时会生成一个唯一的hash值。

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

chunkhash

根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样。(同一个入口文件,构建后同属于一个chunk)

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

contenthash

根据文件的内容生成hash值。不同文件hash值一定不一样

output: {
    filename: 'js/built.[contenthash].js',
    path: resolve(__dirname, 'build')
},
  
new MiniCssExtractPlugin({
  filename: 'css/built.[contenthash].css'
}),

tree shaking

它的作用是去除无用代码,减少代码体积。

如何使用:

  • 必须使用ES6模块化
  • mode 配置 为production环境

由于webpack版本的差异,有的版本会把样式文件和配置文件browserslist当作无用代码。针对这种情况,我们可以在package.json中配置sideEffects

"sideEffects": false 所有代码都没有副作用(都可以进行tree shaking)
       
"sideEffects": ["*.css", "*.less"]

code split

多入口

在entry中配置多入口,webpack会根据不同入口打包生成不同的boundle。

entry: {
    // 多入口:有一个入口,最终输出就有一个bundle
    index: './src/js/index.js',
    test: './src/js/test.js'
  },

  output: {
    // [name]:取文件名
    filename: 'js/[name].[contenthash:10].js',
    path: resolve(__dirname, 'build')

  },

同时在output中对filename输出使用[name] 占位符,这样输出的boundle名会跟入口名保持一致。

optimization

optimization 是 webpack 的配置项之一,用于对打包输出进行优化。它提供了多个配置选项,可以根据项目的需求进行配置。

  • minimize:是否启用代码压缩,默认为 true。设置为 false 可以禁用代码压缩。
  • minimizer:指定用于代码压缩的插件。可以配置多个压缩插件,例如 TerserWebpackPluginOptimizeCssAssetsWebpackPlugin 等。
  • splitChunks:用于拆分代码块的配置。可以将共享的模块拆分成单独的文件,提高缓存利用率和加载速度。可以配置 cacheGroups 进一步细分代码块的拆分规则。
  • runtimeChunk:是否将 webpack 的运行时代码提取为单独的文件,默认为 false。设置为 true 可以将运行时代码提取为独立的文件,可以避免每次构建时运行时代码的变化导致缓存失效。
  • moduleIds:用于生成模块标识符的方式,默认为 'natural'。可以配置为 'named''hashed' 等方式,控制生成的模块标识符的可读性和唯一性。
  • chunkIds:用于生成代码块标识符的方式,默认为 'natural'。可以配置为 'named''hashed' 等方式,控制生成的代码块标识符的可读性和唯一性。

因此我们可以使用splitChunks.chunks来进行代码分割。它有如下可选值:

  • async:将异步加载的模块拆分为单独的文件。
  • initial:将入口模块中的公共模块拆分为单独的文件。
  • all:将异步和入口模块中的公共模块都拆分为单独的文件。

通常情况下,我们配置为all

/* 代码省略 */
optimization: {
    splitChunks: {
      chunks: 'all'
    }
  },
mode: 'production'

import() 动态导入语法

在入口文件中,对模块使用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() {
  import(/* webpackChunkName: 'test'/'./test').then(({ mul }) => {
    console.log(mul(4, 5));
  });
};

预加载等其他资源加载完毕,浏览器空闲了,再偷偷加载资源

import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test').then(({ mul }) => {
    console.log(mul(4, 5));
});

它们都是使用了import()语法

PWA

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

首先我们要使用workbox,在webpack中配置workbox-webpack-plugin

plugins: [
    new WorkboxWebpackPlugin.GenerateSW({
      /*
        1. 帮助serviceworker快速启动
        2. 删除旧的 serviceworker
        生成一个 serviceworker 配置文件~
      */
      clientsClaim: true,
      skipWaiting: true
    })
  ],

然后,我们在入口文件中使用serviceWorker对象

// 注册serviceWorker
// 处理兼容性问题
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/service-worker.js')
      .then(() => {
        console.log('sw注册成功了~');
      })
      .catch(() => {
        console.log('sw注册失败了~');
      });
  });
}

多进程打包

在我们前端项目中,最多的就是js代码,而在js代码中最多的就是Babel的兼容性处理,因此当项目越来越大时,我们需要对babel采用多进程打包。多进程打包,我们使用thread-loader

 {
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
              /* 
                开启多进程打包。 
                进程启动大概为600ms,进程通信也有开销。
                只有工作消耗时间比较长,才需要多进程打包
              */
              {
                loader: 'thread-loader',
                options: {
                  workers: 2 // 进程2个
                }
              },
              {
                loader: 'babel-loader',
                options: {
                  presets: [
                    [
                      '@babel/preset-env',
                      {
                        useBuiltIns: 'usage',
                        corejs: { version: 3 },
                        targets: {
                          chrome: '60',
                          firefox: '50'
                        }
                      }
                    ]
                  ],
                  // 开启babel缓存
                  // 第二次构建时,会读取之前的缓存
                  cacheDirectory: true
                }
              }
            ]
          },

externals

externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。
以jQuery为例:

module.exports = {
  //...
  externals: {
    jquery: 'jQuery',
  },

};

同时我们在index.html 中引入cdn资源

<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>

DLL

DLL(Dynamic Link Library)是一种动态链接库的技术,用于提高代码的复用性和构建速度。它可以将一些稳定不变的第三方库或模块提前打包成独立的 DLL 文件,然后在构建过程中直接引用这些 DLL 文件,而不需要重复构建这些库或模块。这样可以减少构建时间,并提高开发和构建的效率。

使用 DLL 技术的主要步骤包括:

  1. 创建一个用于生成 DLL 文件的配置文件,通常命名为 webpack.dll.config.js。该配置文件中定义了需要打包为 DLL 的库或模块,以及输出的 DLL 文件名和路径。

image.png

以 jQuery 为例 编写dll配置文件:

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]', // 映射库的暴露的内容名称
      path: resolve(__dirname, 'dll/manifest.json') // 输出文件路径
    })
  ],
  mode: 'production'
};

以上代码我们使用webpack提供的DllPlugin 打包生成一个 manifest.json文件。

  1. 运行 webpack 命令,并指定使用上述的配置文件:webpack –config webpack.dll.config.js。这将生成 DLL 文件。

image.png

  1. 在常规的构建过程中,通过引入 DLL 文件,直接使用其中的模块或库,而不需要重新构建。因此我们要修改webpack的配置文件
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'
};

以上代码中,我们使用webpack提供的DllReferencePlugin 读取dll/manifest.json文件,同时告诉webpack哪些库不参与打包。然后我们又使用AddAssetHtmlWebpackPlugin插件把dll/jquery.js在html中自动引入。

使用 DLL 技术可以有效减少构建时间,尤其是对于那些稳定不变的第三方库或模块。它可以在开发过程中提高开发效率,并在构建时加快打包速度。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYto3nFi' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片