《webpack实战调优进阶》 part2 优化部分

Posted lin-fighting

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《webpack实战调优进阶》 part2 优化部分相关的知识,希望对你有一定的参考价值。

代码分片

实现高性能应用其中重要的一点就是尽可能地让用户每次只加载必要的资源,优先级 不太高的资源则采用延迟加载等技术渐进式地获取,这样可以保证页面的首屏速度。

代码分片可以把代码按照特定的形式进行拆分,使用户不必一次全部加载,而是按需加载。

CommonsChunkPlugin webpack4之前内部自带的插件(基本没用了)

提取react

// webpack.config.js
const webpack = require("webpack");
module.exports = 
  entry: 
    app: "./app.js",
    vendor: ["react"],
  ,
  output: 
    filename: "[name].js",
  ,
  plugins: [
    new webpack.optimize.CommonsChunkPlugin(
      name: "vendor",
      filename: "vendor.js",
    ),
  ],
;

在entry加入了一个入口vendor,并使其只包含react,这样vendor和app共有的模块就是react,然后CommonsChunkPlugin.name配置为vendor,这样产生的字语言就会覆盖原有的venfor这个入口产生的资源,最终的效果就是app.js 400字节,vendor.js 73kb(包含了react)

缺陷:

  • 1 一个CommonsChunkPlugin只能提取一个vendor,假如我们想提取多个vendor则需 要配置多个插件,这会增加很多重复的配置代码。

  • 2 由于内部设计上陷,CommonsChunkPlugin在提取公共模块的时候会破坏 掉原有Chunk中模块的依赖关系,导致难以进行更多的优化。比如在异步Chunk的场景下 CommonsChunkPlugin并不会按照我们的预期正常工作。

optimization.SplitChunks

是Webpack 4位了改进CommonsChunkPlugin而重新设计的,功能更加强大,更简单易用。

基本使用:

// webpack.config.ts
import * as path from 'path';
import * as webpack from 'webpack';

import 'webpack-dev-server';


const config: webpack.Configuration = 
    mode: 'development',
    entry: './src/index.js',
    output: 
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    ,
    module: 
        rules: [
            
                test: /\\.(jsx|js)$/,
                exclude: /node_modules/, // 指定转译时忽略的文件夹
                use: [
                  
                    loader: 'babel-loader',
                  ,
                ],
              ,
        ]
    ,

    optimization: 
        splitChunks: 
            "chunks": "all"
        
    


export default config
// .babelrc.js
module.exports = 
  presets: ["@babel/preset-env", "@babel/react"],
;
// 没有使用optimization的结果
asset 31bf811d_main.js 1010 KiB [emitted] [immutable] (name: main) 打包的main.js文件有1000kb大小
//使用之后
asset c5707c6b_vendors-node_modules_react-dom_index_js.js 1010 KiB [emitted] [immutable] (id hint: vendors)
asset c5707c6b_main.js 7.09 KiB [emitted] [immutable] (name: main)
react被单独抽取出来了。

splitChunks跟CommonsChunkPLugin的区别

  • 从命令式到声明式

CommonsChunkPLugin的配置更像是命令式配置,将什么模块什么模块的公用模块提取出来。

而splitChunks更像是生命是变成,只需要设置一些提取条件,如提取的模式,提取的体积,当某些模块达到了这些条件之后就会自动提取出来。

splitChunks的默认提取条件

  • 1 提取后的chunk可被共享,或者来自node_modules目录。比如多次被引用的模块或者在node_modules中的更倾向于共享模块
  • 2 提取后的js chunk体积大于30k(压缩和giip之前), css chunk体积大于50(kb),当提取后的模块满足这两个条件才会被提取,不然提取后体积太小,优化效果一般。
  • 在按需加载过程中,也就是通过动态插入script标签的方式加载脚本,比如异步加载的时候就是通过创建scirpt标签去请求j s文件实现异步加载。一般不希望这个加载的文件大于5,如果大于5,那也是最多只有5个script被并行请求。
  • 首次加载的时候,并行请求不超过3个,什么意思呢?就是打包后的index.html需要加载的文件不能超过3个,也就是不能超过3个script,一般有main.js,第三方模块vendor.js,还有一些公共模块,common.j s,刚好三个,如果设置为2,则common.js不会被单独打包出来。

刚才的react被提取出来,因为react是node_module的,体积也大于30kb,没有按需加载的请求,首次加载并行请求为2,为main.js和vendor-main.foo.js

实际上,splutChunks不配置也可以生效,但仅仅针对异步资源。

// splitChunks的默认配置
splitChunks : 
    chunks: "async",  匹配模式:async默认,initial只对入口chunk生效,all是两种模式都开启,也可以用函数,返回个boolean,表示这个chunk要不要提取。
    minSize:  //最小条件
        javascript: 30000,
        style: 50000,
    ,
    maxSize: 0, //最大条件
    minChunks: 1,  // chunk最小被共享数
    maxAsyncRequests: 5, //	按需加载最大请求
    maxInitialRequests: 3, //首次加载最大并发
    automaticNameDelimiter: '~', 
    name: true, // 为ture,表示可以根据cacheGroups和作用范围自动为新生成的chun命名,并且以automaticNameDelimiter分隔,如vendors~a~b.js就是
    						// cacheGorups为vendors,并且该chunk是由a,b,c三个入口chunk所产生的。
    cacheGroups: 
        vendors: 
            test: /[\\\\/]node_modules[\\\\/]/,
            priority: -10,
        ,
        default: 
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true,//如果匹配到这条,但是已经被打包的,就不需要打包了。
        ,
    ,
,

cacheGroups可以理解成分离chunks的规则,默认情况下有两种,一种是vendors,用于提取所有normoudles中符合条件的模块。而default则作用于被多次引用的模块,当一个模块同时符合多个cacheGroups,根据priority确定优先级。

vue3脚手架的配置是:

// vue3脚手架的配置,很多都是采用默认的。
splitChunks : 
    cacheGroups: 
        vendors: 
            test: /[\\\\/]node_modules[\\\\/]/,
            name: 'vendor',
            priority: -10,
            chunks: 'initial', //打包同步的初始化需要的
        ,
        common: 
            minChunks: 2,
            priority: -20,
           name: 'common-chunks',
             chunks: "initial", //分割的是同步的代码块。
            chunks: 'initial', //打包同步的初始化需要的
        ,
    ,
,

source-map

Webpack对于工程源代码 的每一步处理都有可能会改变代码的位置、结构,甚至是所处文件,因此每一步都需要生 成对应的source map。若我们启用了devtool配置项,source map就会跟随源代码一步步被 传递,直到生成最后的map文件。这个文件默认就是打包后的文件名加上.map,如 bundle.js.map。

并且bundle文件末端会加上一句注释:标识map文件的配置。

//# sourceMappingURL=bundle.js.map

当我们打开了浏览器的开发者工具时,map文件会同时被加载,这时浏览器会使用它 来对打包后的bundle文件进行解析,分析出源代码的目录结构和内容。

module.exports = 
// ...
devtool: 'source-map',
;

这样则启动了source-map,而css less scss需要额外在loader配置。


	loader: 'css-loader',
	options: 
		sourceMap: true,
	,


关键字:

  • 1 eval 使用 eval 包裹模块的代码,每个模块的代码会使用 eval 包裹。eval会生成每个模块的sourcemap,然后用eval包裹,内嵌在js文件中,他可以缓存。
  • 2 source-map 产生.map 文件,每个文件会在最下方使用 SourcemapUrl 标识map文件的地址,map 文件的映射是行列映射。如
      var a = 1;
      var b = 2;

​ 打包后:

      var a = 1;var b = 2;

map文件的映射规则就是原文件:第1行第五个字符串a —对应 — 打包后的文件第一行第五列。简写:1.5-1.5
变量b就是2.5-1.15。映射包含行列信息

  • 3 cheap 轻量的,只用于开发环境,不包含列信息,也不包含loader的sourcemap(只包含打包后的代码到es5的映射,不包含es5到react的映射。),比如上面,a就是1-1,b就是2-1,只有行信息,没有列信息。

  • 4 module 包含loader的sourcemap(比如jsx to js, babel的sourcemap),否则无法定义源文件,
    其他inline eval都默认module的功能,只有cheap不包含。
    比如:react转换为es5的代码之后,再转化为webpack压缩后的代码,cheap-source-map只能映射压缩后的代码到es5的代码,但是开发人员想看的是
    react的代码,所以就可以使用cheap-module-source-map,它会在react到转化后的es5代码之间也做一层sourcemap。

  • 5 inline 将.map作为DataUrl(base64)嵌入,不单独生成.map文件,他跟eval一样都是内置的,但是eval是对每个模块生成对应的sourcemap,用eval包裹,可以缓存,而inline的本质跟source-map一样,都是全部模块
    生成一个sourcemap,只是他作为dataurl内嵌进了js文件里面。

  • 可以任意组合,但是有顺序

组合顺序:

  • [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
    1. source-map
      单独在外部生成"完整"的sorucemap文件,包含完整的行列信息,并在目标文件里建立关联,能提示错误代码的准确原始位置。
      每个模块都有 //# sourceMappingURL=index.js.map,标识对应的map文件
    1. inline-source-map
      以base64格式内联在打包后的文件中,内联构建速度更快,包含完整的行列信息
      在目标文件里建立关系,也能提示错误代码的准确原始位置,不会生成单独的文件
    1. hidden-soruce-map
      会在外部生成sourcemap文件,但是在目标文件里没有建立关联,不能提示错误代码的准确原始位置。没有 //# sourceMappingURL=index.js.map
      上线的代码中不能有soruce-map信息,有可能泄露源代码
      但是在线上出bug需要调试,需要源代码。
    1. eval-source-map
      会为每一个模块生成一个单独的sorucemap进行内联,并且使用eval包裹
    1. nosources-source-map
      也会在外部生成sourcemap文件,能找到原始代码的位置,但是源代码内容为空,可以告诉你错误位置,但是不会泄露源代码。
    1. cheap-source-map
      外部生成sourcemap文件,不包含列信息,不包含loader的转换信息,只能映射到转化后的es5代码。
    1. cheap-module-source-map
      外部生产sourcemap文件,不包含列信息,但是包含loader的map信息,可以映射到转换前的react代码

最佳实践

开发环境,速度快,调试友好

  • 速度快:eval-cheap-source-map 没有包含loader的映射,而且对每个模块的sourcemap进行eval包裹,缓存。
  • 调试更友好 cheap-module-source-map 包含了loader的映射
  • 折中选择:eval-source-map,又有loader的映射,又缓存了每个模块的sourcemap(映射关系)

生成环境

  • 排除内联,因为一方面隐藏了源代码,一方面减少文件体积。
  • 要想调试友好: source-map>cheap-source-map>cheap-module-source-map>hidden-source-map>nosources-source-map
  • 速度快,优先选择cheap
  • 折中选择:hidden-source-map,会生成源代码,但又不会在打包后的js代码中有映射关系。不能调试,但可以通过谷歌的设置,手动加入Map文件去映射。

为什么是eval性能更高?

  • 可以单独缓存Map,重建性能更高,速度更改。
  • source-map无法缓存.map文件,每次都需要重新生成完整的sorucemap文件,因为他把一个chunks的所有map存放到一个文件,只要改一个,缓存就会失效,map就需要重建
  • eval和source-map的内容是一样的,但是可以缓存每一个source-map(映射关系),在重新构建的时候速度更快。每个模块有自己的sourcemap,不会关联一起。
  • 改变一个也不会影响其他的,他不会生成单独的map文件,每个模块的Map都是使用eval内联起来。
  • 生产环境不能用eval,因为它会将map内联在了打包后的文件里面,会泄露源代码。,、
  • 他跟inline一样都是内敛在了js文件,但是inline是所有模块都生成一块单独的base64的代码(相当于内嵌的source-map),而eval是每个模块的map分开,使用eval包裹的

不依赖于webpack,使用插件打包source-map(一般用于测试环境)

  • new webpack.SourceMapDevToolPlugin,可以定义生成的Map文件,并且设置打包后的代码文件与Map文件的关联关系,如 //# sourceMappingUrl=http://127.0.0.1:8081/[url]
  • new FileManagerWebpackPlugin 文件管理插件,可以复制删除文件
  • 最终的效果就是删除打包后的Map文件,将其复制到你想要的位置,启动一个8080的服务可以访问到map文件,然后设置打包后的代码 //# sourceMappingUrl=http://127.0.0.1:8081/[url],然后的map映射到该端口上。
  • 打包后的map文件被你自己存储,映射关系由你决定,如8080端口,这样其他人看到了sourceMappingUrl,也没有map文件,无法访问到源代码,而只有自己可tiao

生产选择

在生产环境中由于我们会对代码进行压缩,而最常见的压缩插件UglifyjsWebpackPlugin目前只支持完全的source-map,因此没有那么多选择,我们只能使用source-map、 hidden-source-map、nosources-source-map这3者之一。

安全性:

  • webpack提供了hidden-source-map,虽然会产出完整的map文件,但不会在bundle.js文件中添加引用,当打开浏览器的devtool的时候也不会加载,需要借助一些第三方服务,将map文件上传到上面,就可以映射了。如Sentry(个错误跟踪平台)

    Sentry支持JavaScript的source map,可以通过它所提供的命 令行工具或者Webpack插件来自动上传map文件。同时还要在工程代码中添加Sentry 对应的工具包,每当JavaScript执行出错时就会上报给Sentry。Sentry在接收到错误后,就 会去找对应的map文件进行源码解析,并给出源码中的错误栈。

  • Nosoruces-source-map,可以在devtool和日志中看到具体的报错行数,但无法看到map文件。

  • 正常打包出source map,然后通过 服务器的nginx设置(或其他类似工具)将.map文件只对固定的白名单(比如公司内网) 开放

资源压缩

在将资源发布到线上环境前,通常都会进行代码压缩,移 除多余的空格、换行及执行不到的代码,缩短变量名,在执行结果不变的前提下将代码替 换为更短的形式。一般正常的代码在uglify之后整体体积都将会显著缩小。同时,uglify之 后的代码将基本上不可读,在一定程度上提升了代码的安全性。

压缩js

在Webpack 4中默认使用了terser的插件terser-webpack-plugin。

const TerserPlugin = require("terser-webpack-plugin");
module.exports = 
  //...
  optimization: 
    // 覆盖默认的 minimizer
    minimizer: [
      new TerserPlugin(  (production默认打开)
        /* your config */
        test: /\\.js(\\?.*)?$/i,
        exclude: /\\/excludes/,
        parallel: true, 允许使用到过进程进行压缩。
        cache: true,开启缓存
      ),
    ],
  ,
;

压缩css

使用mini-css-extract-plugin将样式提取,然后使用optimize-css-assets-webpack-plugin进行压缩。还可以使用purgecss-webpack-plugin,没用的css会删掉。

optimization: 
    minimizer: [new OptimizeCSSAssetsPlugin(
    // 生效范围,只压缩匹配到的资源
    assetNameRegExp: /\\.optimize\\.css$/g,
    // 压缩处理器,默认为 cssnano
    cssProcessor: require('cssnano'),
    // 压缩处理器的配置
    cssProcessorOptions:  discardComments:  removeAll: true  ,
    // 是否展示 log
    canPrint: true,
    )],
    ,
    

缓存

一般是强缓存,协商缓存。但如果开发者对代码进行修改之后希望快点更新到浏览器上,最好的办法就是改变资源的url,迫使客户端解析index.html文件的时候,script的src改变,不得不去服务器拉取最新的资源。

资源hash

一般使用chunkhash作为文件版本号,因为他会对每一个chunk单独计算一个hash。hash是全局唯一的,一些改变全部改变,可能会导致一些没有变化的文件的文件名改变,浪费请求。chunkhash根hash一样有个缺点,一旦一个chunk的一部分代码改变之后,这个chunk打包出来的文件名有包含chunkhash的都会改变,有些内容比如css没改变过也会导致重新编译重新请求,造成浪费,一些css可以使用contenthash,他是根据内容来计算hash值,相比效率就低一点,因为需要通过文件的内容计算hash值

通过hmtl-webpack-plugin动态生成index.html文件。html-webpack-plugin会自动地将我们打包出来的资源名放入生成的index.html中,这样 我们就不必手动地更新资源URL了。

chunkid

理想状态下,通过splitChunks划分的一些第三方模块代码不常变动的,希望这些代码可以在客户端一直使用缓存。

webpack3的时候,每个模块指定的id是按数字递增的,当有 新的模块插入进来时就会导致其他模块的id也发生变化,进而影响了vendor chunk中的内 容。

打包优化

happypack和thread-loader

HappyPack是一个通过多线程来提升Webpack打包速度的工具,webpack在打包的恶时候有一项非常耗时的操作,那就是当通过entry找到入口文件后,使用匹配的loader进行转移,接着进行依赖查找,将查找到的依赖模块,继续匹配loader,然后继续转移,再继续依赖查找,直到没有依赖之后。需要通过一个递归的过程来寻找解析所有的依赖,有些依赖可能没有任何关系,比如import ‘d.css’; import ‘e.css’,但是却必须串行执行,而happyPack就是以这个串行转为并行为入口,充分利用计算资源提升打包速度。
HappyPack适用于那些转译任务比较重的工程,当我们把类似babel-loader和ts-loader 迁移到HappyPack之上后,一般都可以收到不错的效果。而

对于css-loader这些则意义不大,因为原理就是那样,一般css文件很少引用了其他文件。

happypack的使用网上很多,就不举例子,作者也不再维护,这里可是使用thread-loader,使用也简单,原理一样。

thread-loader 把这个loader放在其他loader之前,放置在这个loader之后的loader就会在一个单独的worker池(worker pool)中运行

 * test: //,
 * use: [loader: 'thread-loader', options: worked: 3, xxx]  最多开启3个进程
 * 

一般用于babel-loader和ts-loader,打包速度根据依赖的模块决定,但是一般快很多。可以使用一些webpack分析插件分析哪个loader耗时较长。

缩小作用域:

提升性能有两种方式,增加资源和缩小范围,增加资源指的是更多的cpu和内存,用更多的计算能力缩短任务执行时间,缩小范围针对任务本身,去掉冗余的流程,等等。thread-loader属于增加资源。

  • loader配置的inlcude和exclude,对于js来说,一般把node_nodules排除掉。

  • noParse,像lodash/jquery,内部不会对其他模块有鱼来,这时候可以使用module.noParse取排除掉。这些模块还是会被打包进去,但是webpack不会对其进行任何解析。noParse支持正则,函数,等。

  • IgnorePlugin,排除一些模块,被排除的模块不会被打包进资源文件。像moment,国际化的文件夹local就有几百k,一般用不到的可以排除

    plugins: [
    new webpack.IgnorePlugin(
    resourceRegExp: /^\\.\\/locale$/, // 匹配资源文件
    contextRegExp: /moment$/, // 匹配检索目录
    )
    ],
    

    去掉moment的local文件夹,要使用特定语言时可以手动import 'momnet/local/en’等方式。

缓存:

/**
 * 利用缓存:
 * 1 babel-loader开启缓存,Babel在转义js文件过程中消耗性能较高,将babel-loader执行的效果缓存起来,重新打包就会尝试读取缓存,从而提高构建速度。
 *    use: [loader: 'babel-loadder', options: cacheDirecotry: true]
 *
 * 2 cache-loader,对性能较大的laoder使用此loader,会将结果缓存到磁盘里面。
 *
 * // 缓存(webpack5不需要)挺好用
 * 3 hard-source-webpack-plugin,为模块提供中间缓存,默认存放路劲是node_modules/.cache/hard-source,第一次构建时间没影响,
 *                               第二次的构建时间可以减少80%
 * plugins: [new HardSourceWebpackPlugin()]
 *
 * 4 oneof
 *  module: 
 *          rules: [
 *              
 *              // 以下loader只匹配一个就返回不执行。
 *              oneof: []
 *              
 *          ]
 *      
 */

webpack5之后会开启本地缓存,不需要额外的插件了。

动态链接库与DllPlugin

DllPlugin借鉴了动态链接库的这种思路,对于第三方模块或者一些不常变化的模块, 可以将它们预先编译和打包,然后在项目实际构建过程中直接取用即可。当然,通过 DllPlugin实际生成的还是JS文件而不是动态链接库,取这个名字只是由于方法类似罢了。 在打包vendor的时候还会附加生成一份vendor的模块清单,这份清单将会在工程业务模块 打包时起到链接和索引的作用。

DllPlugin和Code Splitting有点类似,都可以用来提取公共模块,但本质上有一些区 别。Code Splitting的思路是设置一些特定的规则并在打包的过程中根据这些规则提取模 块;DllPlugin则是将vendor完全拆出来,有自己的一整套Webpack配置并独立打包,在实 际工程构建时就不用再对它进行任何处理,直接取用即可。因此,理论上来说,DllPlugin 会比Code Splitting在打包速度上更胜一筹,但也相应地增加了配置,以及资源管理的复杂 度。

tree-shaking

满足条件:

1 Esmodule,现在很多npm包还是使用commonjs形式,无法tree-shaking。

2 禁止babel-loader转化esmodule [@babel/preset-env, modules: false ]

3 tree shaking本身只是为死代码添加上标记,真正去除死代码是通过压缩工具来进行 的。使用我们前面介绍过的terser-webpack-plugin即可。wbepack4后的production已经内置。

开发环境调优

1 webpack-dashboard 控制台友好显示:

plugins: [
new DashboardPlugin()
],

// 启动命令更改

...
"scripts": 
"dev": "webpack-dashboard -- webpack-dev-server"


2 size-plugin输出本次哦古剑的资源体积以及和上次的对比

3 speed-measure-webpack-plugin 输出本次构建的具体耗费时间

4 HRM 自己配置

const webpack = require('webpack');
module.exports = 
// ...
plugins: [
new webpack.HotModuleReplacementPlugin()
],
devServer: 
hot: true,
,
;

在配置了上面的之后,webpack会为每个模块绑定一个module.hot对象,这个对象包含了HRM的api,调用HRM api有两种方式,一是自己手动添加,二是借助一些loader。

// index.js
import  add  from 'util.js';
add(2, 3);
// 手动添加热更新配置
当发现有模块发生变动时,HMR会
使应用在当前浏览器环境下重新执行一遍index.js(包括其依赖)的内容,但是页面本身
不会刷新。
if (module.hot) 
module.hot.accept();

vue-loader以及react-hot-loader都已经内置了HRM。

HRM原理:

wds启动的服务跟浏览器之间维护了一个websocket(全双工通信,服务端主动推送,可以上网查找。),当本地资源发生变化时WDS会向浏览器推 送更新事件,并带上这次构建的hash,让客户端与上一次资源进行比对。

以上是关于《webpack实战调优进阶》 part2 优化部分的主要内容,如果未能解决你的问题,请参考以下文章

前端进阶:一文轻松搞定webpack基础知识进阶与调优

性能测试分析优化的方法论 | 运维进阶

程序员进阶架构师必备架构基础技能:并发编程+JVM+网络+Tomcat等

二十分钟带你了解JVM性能调优与实战进阶

Nginx大并发优化实战

webpack4之路-优化进阶