Optimisation des performances Webpack (build + efficacité de rendu)

Optimisation des performances Webpack

Pourquoi optimiser

Il n'y a pas de mode fixe pour l'optimisation de l'emballage Webpack. En général, nos optimisations courantes sont le déballage, la segmentation, la compression, etc., qui ne s'appliquent pas à tous les projets. Pour des projets spécifiques, un débogage et une optimisation continus sont nécessaires ; les principaux enregistrements ici sont It est l'idée générale, et chaque pièce peut être pratiquée séparément ;

Optimisation de l'efficacité de l'emballage

Comment analyser la vitesse d'emballage

Utilisez-le pour speed-measure-webpack-pluginmesurer le temps passé à différentes étapes lors de la création de votre webpack ;

// 分析打包时间
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
// ...
module.exports = smp.wrap(prodWebpackConfig) // prodWebpackConfig 为webpack的配置项
复制代码

Analyser les raisons qui affectent l'efficacité de l'emballage

  1. Pour commencer l'empaquetage, nous devons obtenir tous les modules dépendants ---- temps de recherche

  2. Analyser tous les modules dépendants (analyser en code exécutable par le navigateur) ----- temps d'analyse

    webpack analyse les fichiers correspondants en fonction de notre chargeur configuré. Dans le développement quotidien, nous devons utiliser le chargeur pour convertir js, css, images, polices et autres fichiers, et la quantité de données de fichiers converties est également très importante. En raison de la nature monothread de js, ces opérations de conversion ne peuvent pas traiter les fichiers simultanément, mais doivent traiter les fichiers un par un.

  3. Regroupez tous les modules dépendants dans un seul fichier ----- temps de compression

    Regroupez tout le code analysé dans un fichier. Afin de mettre à jour le package chargé par le navigateur (réduire le temps d'écran blanc), webpack optimisera le code.

    La compression JS est la dernière étape de la publication et de la compilation. Généralement, Webpack doit être bloqué pendant un certain temps. En effet, la compression JS doit analyser le code dans un arbre de syntaxe AST, puis analyser et traiter l'AST selon des règles complexes, et enfin restaurer l'AST en JS , ce processus implique beaucoup de calculs, il prend donc beaucoup de temps et il est facile de rester bloqué lors de l'emballage

  4. Emballage secondaire----- Deuxième temps d'emballage

    Lors de la modification d'un petit fichier dans le projet, nous devons reconditionner, tous les fichiers doivent être reconditionnés, cela prend le même temps que le package initial, mais la plupart des fichiers du projet restent inchangés, en particulier les bibliothèques tierces

Moyens d'optimisation pour chaque nœud différent

temps de recherche

Réduisez la portée de la recherche de fichiers pour réduire les travaux de compilation inutiles

webpack 打包时,会从配置的 entry 触发,解析入口文件的导入语句,再递归的解析,在遇到导入语句时 webpack 会做两件事情:

  • 根据导入语句去寻找对应的要导入的文件。例如 require('react') 导入语句对应的文件是 ./node_modules/react/react.jsrequire('./util') 对应的文件是 ./util.js
  • 根据找到的要导入文件的后缀,使用配置中的 Loader 去处理文件。例如使用 ES6 开发的 JavaScript 文件需要使用 babel-loader 去处理

主要优化手段

  1. 优化 loader 配置

    使用 Loader 时可以通过 testincludeexclude 三个配置项来命中 Loader 要应用规则的文件

  2. 优化 resolve.module 配置

    resolve.modules 用于配置 webpack 去哪些目录下寻找第三方模块,resolve.modules 的默认值是 ['node_modules'] ,含义是先去当前目录下的 ./node_modules 目录下去找想找的模块,如果没找到就去上一级目录 ../node_modules 中找,再没有就去 ../../node_modules 中找,以此类推

  3. 优化 resolve.alias 配置

    resolve.alias 配置项通过别名来把原导入路径映射成一个新的导入路径,减少耗时的递归解析操作。

  4. 优化 resolve.extensions 配置

    • resolve.extensions 列表要尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中
    • 频率出现最高的文件后缀要优先放在最前面,以做到尽快的退出寻找过程。
    • 在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。
  5. 优化 module.noParse 配置

    module.noParse 配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。 原因是一些库,例如 jQuery 、ChartJS, 它们庞大又没有采用模块化标准,让 Webpack 去解析这些文件耗时又没有意义。

配置示例参考

// 编译代码的基础配置
module.exports = {
  // ...
  module: {
    // 项目中使用的 jquery 并没有采用模块化标准,webpack 忽略它
    noParse: /jquery/,
    rules: [
      {
        // 这里编译 js、jsx
        // 注意:如果项目源码中没有 jsx 文件就不要写 /\.jsx?$/,提升正则表达式性能
        test: /\.(js|jsx)$/,
        // babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
        use: ['babel-loader?cacheDirectory'],
        // 排除 node_modules 目录下的文件
        // node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
        exclude: /node_modules/,
      },
    ]
  },
  resolve: {
    // 设置模块导入规则,import/require时会直接在这些目录找文件
    // 可以指明存放第三方模块的绝对路径,以减少寻找
    modules: [
      path.resolve(`${project}/client/components`), 
      path.resolve('h5_commonr/components'), 
      'node_modules'
    ],
    // import导入时省略后缀
    // 注意:尽可能的减少后缀尝试的可能性
    extensions: ['.js', '.jsx', '.react.js', '.css', '.json'],
    // import导入时别名,减少耗时的递归解析操作
    alias: {
      '@compontents': path.resolve(`${project}/compontents`),
    }
  },
};
复制代码

优化解析时间 - 开启多进程打包

运行在 Node.js 之上的 webpack 是单线程模式的,也就是说,webpack 打包只能逐个文件处理,当 webpack 需要打包大量文件时,打包时间就会比较漫长

  1. thread-loader

    // ...
    const threadLoader = require('thread-loader');
    
    const jsWorkerPool = {
      // options
      
      // 产生的 worker 的数量,默认是 (cpu 核心数 - 1)
      // 当 require('os').cpus() 是 undefined 时,则为 1
      workers: 2,
      
      // 闲置时定时删除 worker 进程
      // 默认为 500ms
      // 可以设置为无穷大, 这样在监视模式(--watch)下可以保持 worker 持续存在
      poolTimeout: 2000
    };
    
    const cssWorkerPool = {
      // 一个 worker 进程中并行执行工作的数量
      // 默认为 20
      workerParallelJobs: 2,
      poolTimeout: 2000
    };
    
    threadLoader.warmup(jsWorkerPool, ['babel-loader']);
    threadLoader.warmup(cssWorkerPool, ['css-loader', 'postcss-loader']);
    
    
    module.exports = {
      // ...
      module: {
        rules: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
              {
                loader: 'thread-loader',
                options: jsWorkerPool
              },
              'babel-loader'
            ]
          },
          {
            test: /\.s?css$/,
            exclude: /node_modules/,
            use: [
              'style-loader',
              {
                loader: 'thread-loader',
                options: cssWorkerPool
              },
              {
                loader: 'css-loader',
                options: {
                  modules: true,
                  localIdentName: '[name]__[local]--[hash:base64:5]',
                  importLoaders: 1
                }
              },
              'postcss-loader'
            ]
          }
          // ...
        ]
        // ...
      }
      // ...
    }
    
    复制代码

优化压缩时间

webpack4 默认内置使用 terser-webpack-plugin 插件压缩优化代码;使用多进程并行运行来提高构建速度。并发运行的默认数量为 os.cpus().length - 1

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true,
      }),
    ],
  },
};
复制代码

合理利用缓存

使用 webpack 缓存的方法有几种,例如使用 DLLcache-loaderHardSourceWebpackPluginbabel-loadercacheDirectory 标志。 所有这些缓存方法都有启动的开销。 重新运行期间在本地节省的时间很大,但是初始(冷)运行实际上会更慢

  • Babel-loader cacheDirectory

    {
      test: /\.js$/,
      use: 'babel-loader?cacheDirectory',
      include: [resolve('src'), resolve('test') ,resolve('node_modules/webpack-dev-server/client')]
    }
    复制代码
  • Cache-loader

    module.exports = {
      module: {
        rules: [
          {
            test: /\.ext$/,
            use: ['cache-loader', ...loaders],
            include: path.resolve('src'),
          },
        ],
      },
    };
    复制代码
  • HardSourceWebpackPlugin --- 二次构建速度会非常的快

页面加载性能优化

如何分析

浏览器渲染原理中入手

image.png

结论:

  1. 网络通信更快 - 网络层面
  2. 拿到数据后,渲染更快 - 渲染层面

关键指标

  1. 白屏时间

    白屏时间节点指的是从用户进入网站(输入url、刷新、跳转等方式)的时刻开始计算,一直到页面有内容展示出来的时间节点;

    这个过程包括dns查询、建立tcp连接、发送首个http请求(如果使用https还要介入TLS的验证时间)、返回html文档、html文档head解析完毕

    • 怎么计算?

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>白屏时间</title>
          <script>
              // 开始时间
              window.pageStartTime = Date.now();
          </script>
          <link rel="stylesheet" href="">
          <link rel="stylesheet" href="">
          <script>
              // 白屏结束时间
              window.firstPaint = Date.now()
          </script>
      </head>
      <body>
          <div>123</div>
      </body>
      </html>
      
      在html文档的head中所有的静态资源以及内嵌脚本/样式之前记录一个时间点,
      在head最底部记录另一个时间点,两者的差值作为白屏时间
      白屏时间 = firstPaint - pageStartTime
      
      复制代码
  2. 首屏时间

    • 首屏时间 = 白屏时间 + 首屏渲染时间

    • 如何计算

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>首屏时间</title>
          <script>
              // 开始时间
              window.pageStartTime = Date.now();
          </script>
          <link rel="stylesheet" href="">
          <link rel="stylesheet" href="">
      </head>
      <body>
          <div>123</div>
          <div>456</div>
          // 首屏可见内容
          <script>
              // 首屏结束时间
              window.firstPaint = Date.now();
          </script>
          // 首屏不可见内容
          <div class=" "></div>
      </body>
      </html>
      由于浏览器解析HTML是按照顺序解析的,当解析到某个元素的时候,
      觉得首屏完成了,就在此元素后面加入<script>计算首屏完成时间
      首屏时间 = firstPaint - pageStartTime
      复制代码
  3. 可操作时间

    // 原生JS实现dom ready
    window.addEventListener('DOMContentLoaded', (event) => {
        console.log('DOM fully loaded and parsed');
    });
    复制代码
  4. 总下载时间

    总下载时间即window.onload触发的时间节点
    复制代码
  5. window.performance

    - memory字段代表JavaScript对内存的占用
    - navigation 统计的是一些网页导航相关的数据
    - timing 它包含了网络、解析等一系列的时间数据
    复制代码
    相关的时间计算:
    DNS查询耗时 = domainLookupEnd - domainLookupStart
    TCP链接耗时 = connectEnd - connectStart
    request请求耗时 = responseEnd - responseStart
    解析dom树耗时 = domComplete - domInteractive
    白屏时间 = domloading - fetchStart
    domready可操作时间 = domContentLoadedEventEnd - fetchStart
    onload总下载时间 = loadEventEnd - fetchStart
    复制代码

优化的手段

网络通信的时间更快

  • 静态资源CDN

    • 优点: 就近原则 & 多级缓存策略

    • 如何配置

      1. index.html
      <body>
          <div id="app"></div>
          <!-- built files will be auto injected -->
          <!--生产环境-->
          <script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js">
          </script>
          <!-- 引入组件库 -->
          <script src="https://cdn.bootcss.com/vue-router/3.2.0/vue-router.min.js">
          </script>
          <script src="https://cdn.bootcss.com/axios/0.23.0/axios.min.js"></script>
          <script src="https://cdn.bootcss.com/element-ui/2.15.6/index.js"></script>
      </body>
      
      2. webpack.config.js
      configureWebpack: {
          externals: {
              "vue": "Vue",
              "vue-router": "VueRouter",
              "axios": "axios",
              "moment": "moment",
              "element-ui": "ELEMENT",
          }
      },
        
      3. 去掉原本代码上的
      import vue from 'vue';
      import vueRouter from 'vue-router';
      复制代码
  • 资源合并 (http2.0就不需要做这个)

  • 域名分片(多域名)(浏览器可并发 6-8个请求 ,http2.0就不需要做这个)

  • 缓存策略

    考虑拒绝一切缓存策略:Cache-Control:no-store
    考虑资源是否每次向服务器请求:Cache-Control:no-cache
    考虑资源是否被代理服务器缓存:Cache-Control:public/private
    考虑资源过期时间:Expires:t/Cache-Control:max-age=t,s-maxage=t
    考虑协商缓存:Last-Modified/Etag
    复制代码
    • 强缓存(资源存储在 memory cache 或 disk cache)
      • Expires: 为http1.0定义的绝对的过期时间(使用本地时间)、本地时间喝服务器时间存在较大差异会错乱
      • Cache-Control: http1.1出现的缓存控制字段、优先级更高、
    • 协商缓存
      • Last-Modified: 资源最后修改时间,当客户端再次请求该资源的时候,会在其请求头上附带上If-Modified-Since字段,值就是之前返回的Last-Modified值。如果资源未过期,命中缓存,服务器就直接返回304状态码,客户端直接使用本地的资源。否则,服务器重新发送响应资源
      • Etag: 通过校验码;这样就保证了在文件内容不变的情况下不会重复占用网络资源。响应头中Etag字段是服务器给资源打上的一个标记,利用这个标记就可以实现缓存的更新。后续发起的请求,会在请求头上附带上If-None-Match字段,其值就是这个标记的值;优先级高
    • Service Worker: Service Worker 是一个相对来说比较新的技术,其目的也主要是为了提高web app的用户体验,可以实现离线应用消息推送等等一系列的功能, 从而进一步缩小与原生应用的差距。 Service Worker可以看做是一个独立于浏览器的Javascript代理脚本,通过JS的控制,能够使应用先获取本地缓存资源(Offline First),从而在离线状态下也能提供基本的功能。 出于安全性的考虑,Service Worker 只能在https协议下使用,不过为了便于开发,现在的浏览器默认支持localhost使用Service Worker。
  • 资源压缩

    • gzip压缩方式 & br
    • 代码文件压缩(注释/空格/变量名)
    • 静态资源(图标,图片资源)
    • 头和报文(http1.1减少不必要的头,减少cookie的数据量)
    • Tree-shaking
    • split-chunk 拆包
  • 通信协议上(http1.0/http1.1/http2.0)

    • 减少http请求

      一个完整的 HTTP 请求需要经历 DNS 查找,TCP 握手,浏览器发出 HTTP 请求,服务器接收请求,服务器处理请求并发回响应,浏览器接收响应等过程

    • 启用HTTP2.0

      1. 二进制协议;解析速度快
      2. 多路复用,多个请求共用一个TCP连接
      3. 首部压缩
  • 按需加载和懒加载

渲染层面

  • 需要使用一个测试工具服务器压力测试(ab) (node --prof 生成整个运行日志,可以被解析成占用率)
  • 前端代码层面
    • html 语义化标签加强dom解析
    • 多用伪元素,减少js操作dom
    • 逻辑和展示解藕;
    • 减少作用域查找和闭包;
    • css文件放置头部,js文件放置在底部
    • js: 算法复杂度
    • web worker: 创造多线程环境
    • 减少重绘和回流
  • 渲染方式 (csr => ssr)-- next/nuxt
    • 优点: 首屏渲染快,SEO 好
    • 缺点: 配置麻烦,增加了服务器的计算压力
  • 静态站点生成方案(SSG)Gatsby/Gridsome

参考文献

Je suppose que tu aimes

Origine juejin.im/post/7084625289682092046
conseillé
Classement