性能优化概述

  1. 构建性能
    1. 构建性能 是指在 开发阶段的构建性能,而不是生产环境的构建性能;
    2. 优化的目标:是降低从打包开始,到代码效果呈现所经过的时间
    3. 构建性能会影响开发效率,构建性能越高,开发过程中时间的浪费越少;
  2. 传输性能
    1. 传输性能 是指打包后的 JS 代码传输到浏览器经过的时间;
    2. 在优化传输性能时要考虑到:
      • 总传输量:所有需要传输的 JS 文件的内容加起来,就是总传输量,重复代码越少,总传输量越少;
      • 文件数量:当访问页面时,需要传输的 JS 文件数量越多,http 请求越多,响应速度越慢;
      • 浏览器缓存JS 文件会被浏览器缓存,被缓存的文件不会再进行传输;
  3. 运行性能
    1. 运行性能是指 JS 代码在浏览器端的运行速度,它主要取决于如何书写高性能的代码;
    2. 永远不要过早的关注于性能,因为在开发的时候,无法完全预知最终的运行性能,过早的关注性能会极大的降低开发效率;

减少模块解析

  1. 什么叫做模块解析?模块解析包括:抽象语法树分析、依赖分析、模块语法替换

  2. 不做模块解析会怎样?

    1. 如果某个模块不做解析,该模块经过 loader 处理后的代码就是最终代码;
    2. 如果没有 loader 对该模块进行处理,该模块的源码就是最终打包结果的代码;
    3. 如果不对某个模块进行解析,可以缩短构建时间;
  3. 哪些模块不需要解析?一些已经打包好的第三方库,比如 jquery 等库

  4. 如何配置让某个模块不要解析?配置 webpack 的 module.noParse 它是一个正则,被正则匹配到的模块不会解析

    // webpack.config.js
    module.exports = {
        mode: "development",
        devtool: "source-map",
        module: {
            noParse: /jquery/
        }
    }
    

优化 loader 性能

限制 loader 应用范围

  1. 对于某些库来说,不需要使用 babel-loader,例如:有些库本身就是用 ES5 语法书写的,不需要转换,使用 babel-loader 转换反而会浪费构建时间;

  2. 配置 webpack 配置文件:以 lodash 为例,lodash 是在 ES5 之前出现的库,使用的是 ES3 语法:

    1. 方式一:通过 module.rule.excludemodule.rule.include 排除或仅包含需要应用 loader 的场景;
      module.exports = {
          module: {
              rules: [
                  {
                      test: /\.js$/,
                      exclude: /lodash/,
                      use: "babel-loader"
                  }
              ]
          }
      }
      
    2. 方式二: 暴力一点,可以排除掉 node_modules 目录中的模块,或仅转换 src 目录的模块,这种做法是对 loader 的范围进行进一步的限制,和 noParse 不冲突
      module.exports = {
          module: {
              rules: [
                  {
                      test: /\.js$/,
                      exclude: /node_modules/,
                      //或
                      // include: /src/,
                      use: "babel-loader"
                  }
              ]
          }
      }
      

缓存 loader 结果

  1. 如果某个文件内容不变,经过相同的 loader 解析后,解析后的结果也不变,于是可以将 loader 的解析结果保存下来,让后续的解析直接使用保存的结果,cache-loader 可以实现这样的功能 cache-loader 还可以实现各自自定义的配置,具体方式见文档

    module.exports = {
      module: {
        rules: [
          {
            test: /\.js$/,
            use: ['cache-loader', ...loaders]
          },
        ],
      },
    };
    
  2. 有趣的是 cache-loader 放到最前面,却能够决定后续的 loader 是否运行,实际上 loader 的运行过程中,还包含一个 pitch 过程;

为 loader 开启多线程

  1. thread-loader 会开启一个线程池,线程池中包含适量的线程,它会把后续的 loader 放到线程池的线程中运行,以提高构建效率;

  2. 由于后续的 loader 会放到新的线程中,所以后续的 loader

    1. 不能使用 webpack api 生成文件;
    2. 无法使用自定义的 plugin api
    3. 无法访问 webpack options
  3. 在实际的开发中,可以进行测试,来决定 thread-loader 放到什么位置;

  4. 特别注意:开启和管理线程需要消耗时间,在小型项目中使用 thread-loader 反而会增加构建时间;

热替换 HMR

  1. 热替换并不能降低构建时间 (可能还会稍微增加),但可以降低代码改动到效果呈现的时间;

  2. 当使用 webpack-dev-server 时,考虑代码改动到效果呈现的过程,而使用了热替换后,流程发生了变化

使用、原理

  1. HMR 使用:更改 webpack 配置

    module.exports = {
      devServer:{
        hot: true // 开启 HMR
      },
      plugins:[ 
        // 可选,不写 webpack4 会内部会根据 devServer.hot 使用热更新
        new webpack.HotModuleReplacementPlugin()
      ]
    }
    
  2. HMR 原理

    1. 当开启了热更新后 webpack-dev-server 会向打包结果中注入 module.hot 属性,默认情况下 webpack-dev-server 不管是否开启了热更新,当重新打包后,都会调用 location.reload 刷新页面,但如果运行了 module.hot.accept() 将改变这一行为;
    2. module.hot.accept() 的作用是让 webpack-dev-server 通过 socket 管道,把服务器更新的内容发送到浏览器,然后将结果交给插件 HotModuleReplacementPlugin 注入的代码执行,插件 HotModuleReplacementPlugin 会根据覆盖原始代码,然后让代码重新执行,所以热替换发生在代码运行期;

样式热替换

  1. 对于样式也是可以使用热替换的,但需要使用 style-loader,因为热替换发生时 HotModuleReplacementPlugin 只会简单的重新运行模块代码;

  2. 因此 style-loader 的代码一运行,就会重新设置 style 标签元素中的样式,而 mini-css-extract-plugin 由于它生成文件是在 构建期间,运行期间也无法改动文件,因此它对于热替换是无效的;

打包公共模块

  1. 什么是分包:将一个整体的代码,分布到不同的打包文件中;

  2. 为什么分包?:减少公共代码,降低总体积,充分利用浏览器缓存,特别是一些大型的第三方库;

  3. 什么时候分包:多个 chunk 引入了相同的公共模块 (会将公共模块打包到多个 chunk 中),公共模块 体积较大改动较少

基本原理

  1. 先单独的打包公共模块,公共模块会被打包成为动态链接库 (dll Dynamic Link Library),并生成资源清单;

  2. 根据入口模块进行正常打包,由于资源清单中包含 jquerylodash 两个模块,因此打包结果的大致格式是 (jquery、lodash源码并不会被打包进来,而是使用 全局变量的方式)

    (function(modules){
      //...
    })({
      // index.js 文件的打包结果并没有变化
      "./src/index.js":
      function(module, exports, __webpack_require__){
        var $ = __webpack_require__("./node_modules/jquery/index.js")
        var _ = __webpack_require__("./node_modules/lodash/index.js")
        _.isArray($(".red"));
      },
      // 由于资源清单中存在,jquery 的代码并不会出现在这里,否则 jquery 源码会被打包进来
      "./node_modules/jquery/index.js":
      function(module, exports, __webpack_require__){
        module.exports = jquery; // 全局变量 jquery
      },
      // 由于资源清单中存在,lodash 的代码并不会出现在这里,否则 lodash 源码会被打包进来
      "./node_modules/lodash/index.js":
      function(module, exports, __webpack_require__){
        module.exports = lodash; // 全局变量 lodash
      }
    })
    

基本使用

  1. 具体流程:

    1. 开启 output.library 暴露公共模块;
    2. 利用 DllPlugin 生成资源清单 (示例:jquery.manifest.json 会和公共模块生成映射关系)
      {
        "name": "jquery",
        "content": {
          "./node_modules/jquery/dist/jquery.js": {
            "id": 1,
            "buildMeta": {
              "providedExports": true
            }
          }
        }
      }
      
    3. DllReferencePlugin 使用资源清单,控制打包结果;
    4. 重新设置 clean-webpack-plugin,避免它把公共模块清除;
    5. 在页面中手动引入公共模块;
  2. 关键代码:

    JavaScript
    JavaScript
    JSON
    html
    // webpack.dll.config.js
    const webpack = require("webpack");
    const path = require("path");
    
    module.exports = {
      mode: "production",
      entry: {
        jquery: ["jquery"],
        lodash: ["lodash"]
      },
      output: {
        filename: "dll/[name].js",
        library: "[name]" // 1. 开启 output.library 暴露公共模块
      },
      plugins: [
        new webpack.DllPlugin({ // 2. 利用 DllPlugin 生成资源清单
          path: path.resolve(__dirname, "dll", "[name].manifest.json"), // 资源清单的保存位置 dll 文件夹下
          name: "[name]"  // 资源清单中,暴露的变量名
        })
      ]
    };
    
    // webpack.config.js
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    const { CleanWebpackPlugin } = require("clean-webpack-plugin");
    const webpack = require("webpack");
    
    module.exports = {
      mode: "development",
      devtool: "source-map",
      entry: {
        main: "./src/index.js",
        other: "./src/other.js"
      },
      output: {
        filename: "[name].[hash:5].js"
      },
      plugins: [
        // 4. 重新设置 clean-webpack-plugin,避免它把公共模块清除,需要做出以下配置
        new CleanWebpackPlugin({
          // 要清除的文件或目录
          // 排除掉dll目录本身和它里面的文件,目录和文件的匹配规则是 globbing patterns
          cleanOnceBeforeBuildPatterns: ["**/*", "!dll", "!dll/*"]
        }),
        new HtmlWebpackPlugin({
          template: "./public/index.html"
        }),
        // 3. 使用 DllReferencePlugin 控制打包结果,这样 jquery、lodash 才不会被打包到 index.js 中
        new webpack.DllReferencePlugin({
          manifest: require("./dll/jquery.manifest.json") // 使用资源清单
        }),
        new webpack.DllReferencePlugin({
          manifest: require("./dll/lodash.manifest.json") // 使用资源清单
        })
      ]
    };
    
    "scripts": {
      "dev": "webpack",
      "dll": "webpack --config webpack.dll.config.js" // 打包公共模块是一个 独立的 打包过程
    },
    
    <!-- 5. 在页面中手动引入公共模块 -->
    <script src="./dll/jquery.js"></script>
    <script src="./dll/lodash.js"></script>
    

优缺点

  1. 优点

    1. 极大提升自身模块的打包速度;
    2. 极大的缩小了自身文件体积;
    3. 有利于浏览器缓存第三方库的公共代码;
  2. 缺点

    1. 使用非常繁琐,麻烦 (不要对小型的公共 JS 库使用)
    2. 如果第三方库中包含了复杂的依赖关系,则效果不太理想;

手动分包、自动分包

  1. 分包后主包的体积减小,可以减小每个页面或模块的初始加载时间,因为用户只需下载当前页面或模块所需的代码,而不必加载整个应用程序的代码;

  2. 这降低了首次加载的时间,提高了用户体验;

手动分包

  1. Webpack 中,可以手动分割代码块 (称为"手动分包")

  2. 手动分包通常通过 import() 函数动态导入实现,这样 Webpack 会将目标模块及其依赖作为一个新的分块进行打包;

    JavaScript
    JavaScript
    // eg1:动态导入 util 模块
    import('./path/to/util').then(({ someFunction }) => {
      someFunction();
    });
    
    // eg2:vue-router 路由表
    const routes = [
      {
        path: '/main/:type',
        component: () => import(/* webpackChunkName: 'MenuWrap' */ '@/view/MenuWrap.vue'),
      },
      {
        path: '/customLogin',
        component: () => import(/* webpackChunkName: 'MenuWrap' */ '@/view/custom/login.vue'),
      },
    ]
    

自动分包

  1. 自动分割则是通过 WebpackSplitChunksPlugin 插件实现的,它默认在 Webpack 中启用 (自动分割通常被称为"自动分包")

  2. 可以通过配置 Webpackoptimization.splitChunks 选项来自定义分割规则:

    // webpack.config.js
    module.exports = {
      // ...
      optimization: {
        splitChunks: {
          chunks: 'async', // async:只分割异步代码块,all:分割全部
          minSize: 30000, // 最小模块大小
          maxSize: 50000, // 最大模块大小(分包的 基础单位是模块 ,如果一个完整的模块超过了该体积,它是无法做到再切割的,尽管使用了这个配置,完全有可能某个包还是会超过这个体积)
          minChunks: 1, // 最小共享模块次数
          maxAsyncRequests: 5, // 最大异步请求数
          maxInitialRequests: 3, // 最大初始化请求数
          automaticNameDelimiter: '~', // 分割后的块名称分隔符
          name: true, // 是否使用模块名作为块名
    
          // webpack 默认提供了两个缓存组
          cacheGroups: {
            // 属性名是缓存组名称,会影响到分包的 chunk 名
            // 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置
            vendors: {
              test: /[\\/]node_modules[\\/]/, // 当匹配到相应模块时,将这些模块进行单独打包
              priority: -10 // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为 0
            },
            default: {
              minChunks: 2,  // 覆盖全局配置,将最小 chunk 引用数改为 2
              priority: -20, // 优先级
              reuseExistingChunk: true // 重用已经被分离出去的 chunk,无需再次分之前分好的包
            }
          }
        }
      }
      // ...
    };
    

单模块体积优化

代码压缩

  1. Webpack 中可以使用 TerserPlugin 来压缩 JavaScript 代码,这个插件会通过 terser 库来移除死代码、最小化变量名、压缩表达式等;

  2. Webpack 相关配置;

    const TerserPlugin = require('terser-webpack-plugin');
    
    module.exports = {
      optimization: {
        minimize: true,
        minimizer: [
          new TerserPlugin({
            terserOptions: {
              compress: {
                drop_console: true, // 移除所有的`console`语句
              },
              output: {
                comments: false, // 去掉注释
              },
            },
            extractComments: false, // 不从代码中提取注释
          }),
        ],
      },
    };
    

tree shaking

  1. 本质上为了消除无用的 JS 代码,减少加载文件体积的方式,使其整体执行时间更短;因为 ES Module 模块化的出现,ES Module 模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析;

    • “CommonJs 是动态的”:模块依赖关系的建立发生在代码运行阶段,require 路径可以是表达式;
    • “ESModule 是静态的”:模块依赖关系的建立发生在代码编译阶段,ESModule 导入、导出语句都是声明式的,必须位于模块的顶层作用域不能放到 if 语句中;
  2. 当打包环境为 production 时,默认开启 tree-shaking 功能;

  3. webpack 在使用 tree shaking 的时候有一个原则:一定要保证代码正确运行,在满足该原则的基础上,再来决定如何 tree shaking

    1. 什么是副作用?一个函数会、或者可能会对函数外部变量产生影响的行为
    2. 副作用代码不可被删除,可能会导致报错或者 bug 的出现;
      const setTitle = () => {
        document.title = "RICE-PUDDING";
      }
      
      // 虽然 a 变量没有被其他地方使用,但由于副作用,如果将其删除,会导致 document.title 没有成功被设置可能导致出现 bug
      const a = setTitle();
      
    3. 所以要清楚的知道代码是否存在副作用,以下代码是针对不同情况的 tree-shaking,在 package.json 中设置 sideEffects
      JSON
      JSON
      JSON
      // 所有文件都有副作用,全都不可 tree-shaking
      {
        "sideEffects": true
      }
      
      // 没有文件有副作用,全都可以 tree-shaking
      {
        "sideEffects": false
      }
      
      // 只有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件
      {
        "sideEffects": [
          "./src/file1.js",
          "./src/file2.js"
        ]
      }
      

多入口打包

  1. webpack的多入口配置具有以下用途:

    1. 合理控制包的大小:通过多入口打包,可以避免一次性加载太多不需要的代码,从而优化项目的加载速度和性能;
    2. 代码复用:多入口配置允许在多个入口起点之间复用大量的代码或模块,提高代码的复用率和效率;
    3. 分割代码:对于大型项目,尤其是包含多个独立功能或模块的项目,如用户和管理员部分,多入口可以使得每个部分被单独打包,生成不同的文件,这样可以根据需要按需加载,进一步优化用户体验和性能;
  2. 总的来说 webpack 的多入口配置提供了一种灵活的方式来组织和管理大型项目的代码,使得项目结构更加清晰,同时也便于代码的复用和优化;

懒加载、预加载

  1. 懒载入预载入webpack 的两个优化策略,可以提高页面加载性能;

  2. 懒载入:懒载入是指在需要的时候才加载某个模块,这样可以减少初始页面加载的大小;在 webpack 中,可以使用动态 import 函数实现懒载入;

    // 假设有一个组件叫做 'async-component.js' 想要懒加载它
    
    // 在某个文件中,比如主文件 index.js
    function getComponent() {
      return import('./async-component.js').then(({ default: asyncComponent }) => {
        // 在这里你可以做任何对 asyncComponent 的操作,比如挂载到 DOM 等
        // 比如:document.body.appendChild(asyncComponent);
      });
    }
    
    // 可以在某个事件触发时调用 getComponent 函数来实现懒加载
    document.getElementById('load-btn').addEventListener('click', getComponent);
    
  3. 预载入:预载入是在页面加载的初期就加载一些将来可能会用到的资源,如 webpack 打包的某个模块;这样在将来需要使用时,资源已经加载好,可以立即使用,降低页面加载时的等待时间 (懒加载实现了 js 文件按需加载,在需要使用时才进行加载,但是如果 js 文件非常大加载速度比较慢,在使用时再加载就会使页面出现卡顿,这时就需要使用预加载)

    document.getElementById('load-btn').onclick = function () {
      // 在浏览器空闲的时候会自动去请求 async-component.js,即使没有触发点击事件的情况下
      import(/*webpackChunkName:'test' ,webpackPrefetch:true*/"./async-component.js");
        .then(({ test }) => {
          console.log('test加载成功');
        })
        .catch(error => {
          console.log('test加载失败 error:', error);
        })
    }
    
  4. webpack 配置文件中,还需要添加一些配置,这样可以确保 webpack 可以正确地将 懒加载、预加载 的代码分割出来;

    module.exports = {
      // ...
      optimization: {
        splitChunks: {
          chunks: 'all',
        },
      },
      // ...
    };
    

gzip 压缩

  1. BS 结构中的压缩传输:服务端获取资源需要先压缩,再发送给客户端 (服务器的压缩需要时间,客户端的解压需要时间)

  2. 使用 webpackcompression-webpack-plugin 插件对打包结果进行预压缩,可以移除服务器的压缩时间,下面是 webpack 的配置;

    const { CleanWebpackPlugin } = require("clean-webpack-plugin");
    const CmpressionWebpackPlugin = require("compression-webpack-plugin");
    
    module.exports = {
      mode: "production",
      optimization: {
        splitChunks: {
          chunks: "all"
        }
      },
      plugins: [
        new CleanWebpackPlugin(),
        new CmpressionWebpackPlugin({
          test: /\.js/,
          minRatio: 0.5
        })
      ]
    };
    
打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 性能优化概述
  2. 2. 减少模块解析
  3. 3. 优化 loader 性能
    1. 3.1. 限制 loader 应用范围
    2. 3.2. 缓存 loader 结果
    3. 3.3. 为 loader 开启多线程
  4. 4. 热替换 HMR
    1. 4.1. 使用、原理
    2. 4.2. 样式热替换
  5. 5. 打包公共模块
    1. 5.1. 基本原理
    2. 5.2. 基本使用
    3. 5.3. 优缺点
  6. 6. 手动分包、自动分包
    1. 6.1. 手动分包
    2. 6.2. 自动分包
  7. 7. 单模块体积优化
    1. 7.1. 代码压缩
    2. 7.2. tree shaking
  8. 8. 多入口打包
  9. 9. 懒加载、预加载
  10. 10. gzip 压缩