Webpack 性能优化

栏目: IT技术 · 发布时间: 4年前

内容简介:本篇文章我们一起来看看

本篇文章我们一起来看看 Webpack 的性能优化相关内容。在这之前,再简单介绍下 Webpack 的一些相关概念。

一、webpack 是什么?

webpack 是一种前端资源构建工具,它是一个 静态模块打包器module bundler )。在 webpack 中,前端的所有资源文件( javascript/json/css/img/less/... )都会作为模块处理,当 webpack 处理应用程序时,它会递归地构建一个依赖关系图( dependency graph ),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

Webpack 性能优化

(图片来自网络)

1、基本功能

  • 代码转换 :将 TypeScript 编译成 JavaScriptSCSS 编译成 CSS 等。
  • 文件优化 :压缩 JavaScriptCSSHTML 代码,压缩合并图片等。
  • 代码分割 :提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载。
  • 模块合并 :在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件。
  • 自动刷新 :监听本地源代码的变化,自动重新构建、刷新浏览器。
  • 代码校验 :在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过。
  • 自动发布 :更新完代码后,自动构建出线上发布代码并传输给发布系统。

2、打包原理

  • 识别入口文件,分析代码,获取模块依赖,并且将代码打包为浏览器可以识别的代码;
  • 递归地构建一个依赖关系图( dependency graph ),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

二、核心概念

1、entry

入口( Entry ):指示 webpack 应该以哪个文件(模块)为入口起点开始打包,分析构建内部依赖图。

2、output

输出( Output ):指示 Webpack 打包后的资源( bundles )输出到哪里去,以及如何命名。基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。

3、module

模块,在 Webpack 里一切皆模块, 一个模块对应着一个文件Webpack 会从配置的 entry 开始递归找出所有依赖的模块。

4、chunk

代码块,一个 chunk 由多个模块组合而成,用于 代码合并与分割

5、loader

Loader 是模块转换器,用于把模块原内容按照需求转换成新内容 。例如:我们会使用 LoaderWebpack 能够去处理那些非 JavaScript 文件( Webpack 自身只理解 JavaScript )。然后就可以利用 webpack 的打包能力,对它们进行处理。

6、plugins

用于扩展 Webpack 功能,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。

7、mode

模式( Mode )指示 Webpack 使用相应模式的配置,是使用开发模式( development )还是生产模式( production )。

  • 开发模式
    • 方便于浏览器调试的工具;
    • 可以快速地对增加的内容进行编译;
    • 提供了更精确、更有用的运行时错误提示机制。
  • 生产模式
    • 自动压缩构建输出的文件
    • 快速的运行时处理
    • 不暴露源代码和源文件的路径
    • 快速的静态资源输出

8、devServer

通过 devServer 启动的 Webpack 会开启监听模式,当发生变化时重新执行构建,然后通知 devServer 会让 Webpack 在构建出的 JavaScript 代码里注入一个代理客户端用于控制网页,网页和 devServer 之间通过 WebSocket 协议通信,以方便 devServer 主动向客户端发送命令。 devServer 在收到来自 Webpack 的文件变化通知时,通过注入的客户端控制网页刷新。

HTTP
SourceMap

三、构建流程

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程 :

  • entry 里配置的 module 开始递归解析 entry 依赖的所有 module

  • 每找到一个 module ,就会根据配置的 loader 去找对应的转换规则

  • module 进行转换后,再解析出当前 module 依赖的 module

  • 这些模块会以 entry 为单位分组,一个 entry 和其所有依赖的 module 被分到一个 Chunk

  • 最后 webpack 会把所有 Chunk 转换成文件输出

  • 在整个流程中 webpack 会在恰当的时机执行 plugin 里定义的逻辑

四、开发环境性能优化

  • 优化打包构建速度
    • HMR
  • 优化代码调试
    • source-map

1、HMR

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

  • 对于样式文件,我们不用手动再启动 HMR 功能,因为 style-loader 内部已经实现了;
  • 对于 JavaScript 文件,默认是不能使用 HMR 功能的,需要修改 JavaScript 代码,添加支持 HMR 功能的代码;注意: HMR 功能对 JavaScript 的处理,只能处理非入口 JavaScript 文件的其他文件。

例子: https://github.com/IDeepspace/webpack-performance-enhancement-example/tree/master/HMR

2、source-map

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

内联和外部的区别:

  • 外部生成了文件,内联没有
  • 内联构建速度更快

用法区别:

  • source-map :外部,可以显示错误代码准确信息和源代码的错误位置;

  • inline-source-map :内联,只生成一个内联 source-map ,可以显示错误代码准确信息和源代码的错误位置;

  • hidden-source-map :外部,显示错误代码错误原因,但是没有错误位置,不能追踪源代码错误,只能提示到构建后代码的错误位置;

  • eval-source-map :内联,每一个文件都生成对应的 source-map ,都在 eval ,错误代码准确信息 和 源代码的错误位置,只是多了一个 hash 值;
  • nosources-source-map :外部,显示错误代码准确信息,但是没有任何源代码信息;
  • cheap-source-map :外部,显示错误代码准确信息和源代码的错误位置,只能精确到行;
  • cheap-module-source-map :外部,错误代码准确信息 和 源代码的错误位置, module 会将 loadersource map 加入。

如何选择

  • 开发环境:速度快( eval>inline>cheap>... ),调试更友好(可以显示源代码信息)

    • 速度快: eval-cheap-source-mapeval-source-map
    • 调试更友好: source-mapcheap-module-source-mapcheap-source-map
    • 平衡点: eval-source-map / eval-cheap-module-source-map
  • 生产环境:隐藏源代码?调试要不要更友好?

    • 内联会让代码体积变大,所以在生产环境不用内联
    • nosources-source-map 全部隐藏
    • hidden-source-map 只隐藏源代码,会提示构建后代码错误信息
    • 也需要平衡: source-map / cheap-module-source-map

例子: https://github.com/IDeepspace/webpack-performance-enhancement-example/tree/master/source-map

五、生产环境性能优化

  • 优化打包构建速度
    oneOf
    babel
    externals
    dll
    
  • 优化代码运行的性能
    hash-chunkhash-contenthash
    tree shaking
    code splitting
    pwa
    

1、oneOf

每个不同类型的文件在 loader 转换时,都会被命中,遍历 modulerules 中所有 loader ,这也会影响性能。使用 oneOf 之后,当规则匹配时,只使用第一个匹配规则。

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.css$/,
        oneOf: [
          {
            resourceQuery: /inline/, // foo.css?inline
            use: 'url-loader'
          },
          {
            resourceQuery: /external/, // foo.css?external
            use: 'file-loader'
          }
        ]
      }
    ]
  }
};

注意:

  • 使用 oneOf 根据文件类型加载对应的 loader 时,只要能匹配一个即可退出;
  • 对于同一类型文件,比如处理 js ,如果需要多个 loader ,可以单独抽离 js 处理,确保 oneOf 里面一个文件类型对应一个 loader
  • 可以配置 enforce: 'pre', 指定优先执行。

例子: https://github.com/IDeepspace/webpack-performance-enhancement-example/tree/master/oneOf

2、缓存

babel 缓存

因为在生产环境中不能使用 HMR ,要想也达到 HMR 的效果,在更改一个模块之后,不至于让别的模块也重新打包,可以使用 babel 缓存。

use: {
   loader: 'babel-loader',
   options: {
     cacheDirectory: true
   }
 }

这样在第二次构建的时候,会读取之前的缓存,加快打包速度。

文件资源缓存

一般情况下,对于前端静态资源,浏览器访问的时候希望资源都能够进行缓存,当第二次及以后进入页面的时候,页面就可以直接使用缓存资源,这样的话,页面打开速度很快,提高了用户体验同时也节省了带宽资源。而其中最为常见的一种最大化利用缓存的形式就是为静态资源加上 hash ,使用一个不会重复的标识符来达到资源可以永久缓存的目的。

Webpack 打包的时候,我们可以给打包的文件名加上一个 hash 值,有两种方式:

  • hash :每次 wepack 构建时会生成一个唯一的 hash

    • 问题:因为 jscss 同时使用一个 hash 值;如果重新打包,会导致所有缓存失效(可能我却只改动一个文件)
  • chunkhash :根据 chunk 生成的 hash 值。如果打包来源于同一个 chunk ,那么 hash 值就一样

    • 如何 css 是在 js 中被引入的,所以同属于一个 chunkjscsshash 值还是一样的。
  • contenthash :根据文件的内容生成 hash 值。不同文件 hash 值一定不一样,这会让代码上线运行缓存更好使用(上线代码性能优化)。

例子: https://github.com/IDeepspace/webpack-performance-enhancement-example/tree/master/cache

3、多进程打包

thread-loader 一般是给 loader 用的。下面是一个在 babel-loader 中使用多进程打包的例子:

{
  test: /\.js$/,
    exclude: /node_modules/,
      use: [
        {
          loader: 'thread-loader',
          options: {
            workers: 2 // 进程2个
          }
        },
        {
          loader: 'babel-loader',
          options: {
            // 预设:指示babel做怎么样的兼容性处理
            presets: [
              [
                '@babel/preset-env',
                {
                  // 按需加载
                  useBuiltIns: 'usage',
                  // 指定core-js版本
                  corejs: {
                    version: 3
                  },
                  // 指定兼容性做到哪个版本浏览器
                  targets: {
                    chrome: '60',
                    firefox: '60',
                    ie: '9',
                    safari: '10',
                    edge: '17'
                  }
                }
              ]
            ],
            // 开启babel缓存
            // 第二次构建时,会读取之前的缓存
            cacheDirectory: true
          }
        }
      ]
},

但是要注意,启动进程是需要时间的,进程间的通信也是需要时间的,如果本来构建时间就很快,这种情况下,不需要使用 thread-loader

例子: https://github.com/IDeepspace/webpack-performance-enhancement-example/tree/master/thread-loader

4、externals

externals 的作用是防止将某一些包打包到我们的输出中。假设我们的项目中使用了 JQuery ,我们不希望将它打包到输出文件中,而是使用外部的 CDN 链接。

// 忽略打包的文件
externals: {
  // 拒绝jQuery被打包进来
  // 忽略库名 -- npm 包名
  jquery: 'jQuery'
}

注意,当我们忽略打包文件需要在 HTML 中使用 script 标签将其引入进来的。

例子: https://github.com/IDeepspace/webpack-performance-enhancement-example/tree/master/externals

5、dll

使用 dll 技术,可以对某些库(一般是第三方库: jqueryreactvue …)进行单独打包。

基本过程

  • 第一次的时候把请求的内容存储起来存储在映射表中;

  • 再次请求时,先从映射表中找请求的内容,看是否有缓存,有则加载缓存(类似浏览器的缓存策略命中缓存),没有就正常打包。

缺点:配置很麻烦。

例子: https://github.com/IDeepspace/webpack-performance-enhancement-example/tree/master/dll

5、tree shaking

tree shaking 的作用是去除无用代码,让代码的体积更小。开启 production 环境后,默认就会启动 tree shaking

但是对于 JavaScript 文件来说,还有一个前提条件:必须使用 ES6 模块化。

最后还有一个问题需要注意,可能由于 Webpack 版本的原因,会把一些 css 资源给 shaking 掉,所以最好在 package.json 中配置一下:

"sideEffects": [
  "*.css",
  "*.less"
]

例子: https://github.com/IDeepspace/webpack-performance-enhancement-example/tree/master/tree-shaking

6、code splitting

在最开始使用 Webpack 的时候,都是将所有的 js 文件全部打包到一个 build.js 文件中,但是在大型项目中, build.js 可能过大, 导致页面加载时间过长。这个时候就需要 code splitting

code splitting 就是将文件分割成块( chunk ), 我们可以定义一些分割点( split point ),根据这些分割点对文件进行分块,并实现按需加载。

大致有三种方式来配置:

  • 添加多个入口 entry
    • 缺点:不好维护,每次都需要新添加入口配置
  • 使用 splitChunks 配置
    • 可以将 node_modules 中代码单独打包一个 chunk 最终输出
    • 它也会自动分析多入口 chunk 中,有没有公共的文件,如果有则会打包成单独一个 chunk
  • 通过 js 代码,让某个文件被单独打包成一个 chunk
    • import 动态导入语法:能将某个文件单独打包

例子: https://github.com/IDeepspace/webpack-performance-enhancement-example/tree/master/code-splitting

7、懒加载和预加载

  • 懒加载(延迟加载):当文件需要使用时才加载。

    document.getElementById('btn').onclick = function() {
      import(/* webpackChunkName: 'test' */'./test').then(({ mul }) => {
        console.log(mul(4, 5));
      });
    };
  • 预加载 prefetch :会在使用之前,提前加载 js 文件,等其他资源加载完毕,浏览器空闲了,再偷偷加载资源(兼容性比较差)。

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

https://github.com/IDeepspace/webpack-performance-enhancement-example/tree/master/lazy-loading

8、PWA

渐进式网络开发应用程序(离线可访问),使用 workbox-webpack-plugin 插件来实现。有两个步骤:

1、在 Webpack 中配置:

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

2、注册 serviceWorker ,并处理兼容性问题:

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

另外, serviceWorker 必须在服务器上。

例子: https://github.com/IDeepspace/webpack-performance-enhancement-example/tree/master/pwa


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web前端开发最佳实践

Web前端开发最佳实践

党建 / 机械工业出版社 / 2015-1 / 59.00元

本书贴近Web前端标准来介绍前端开发相关最佳实践,目的在于让前端开发工程师提高编写代码的质量,重视代码的可维护性和执行性能,让初级工程师从入门开始就养成一个良好的编码习惯。本书总共分五个部分13章,第一部分包括第1章和第2章,介绍前端开发的基本范畴和现状,并综合介绍前端开发的一些最佳实践;第二部分为第3-5章,讲解HTML相关的最佳实践,并简单介绍HTML5中新标签的使用;第三部分为第6-8章,介......一起来看看 《Web前端开发最佳实践》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

MD5 加密
MD5 加密

MD5 加密工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具