Webpack优化实践

作者: A郑家庆 | 来源:发表于2021-08-25 16:37 被阅读0次

    前言

    随着项目不断迭代,项目体积会越来越大,导致项目构建速度和浏览器打开页面的时间也随着变长,所以需要对 webpack构建进行优化。

    Webpack是什么

    webpack是一个静态资源打包工具,负责将项目中依赖的各个模块,打包成一个或多个文件。

    加快构建速度

    一、缩小文件的搜索范围

    优化Loader配置

    定义:webpack 只能直接处理 javascript 格式的代码。任何非 js 文件都必须被预先处理转换为 js 代码,才可以参与打包。loader(加载器)就是这样一个代码转换器。
    由于Loader对文件的转换操作很耗时,所以需要让尽可能少的文件被Loader处理。我们可以通过以下3方面优化Loader配置:(1)优化正则匹配(2)通过cacheDirectory选项开启缓存(3)通过include、exclude来减少被处理的文件。实践如下:
    项目原配置:

    {
      test: /\.js$/,
      loader: 'babel-loader',
      include: [resolve('src'), resolve('test')]
    }
    

    优化后配置:

    // webpack.base.conf.js
    {
      // 1、如果项目源码中只有js文件,就不要写成/\.jsx?$/,以提升正则表达式的性能
      test: /\.js$/,
      // 2、babel-loader支持缓存转换出的结果,通过cacheDirectory选项开启
      loader: 'babel-loader?cacheDirectory',
      // 3、只对项目根目录下的src 目录中的文件采用 babel-loader
      include: [resolve('src')],
      exclude: /node_modules/
    }
    
    优化resolve.modules配置

    resolve.modules 用于配置Webpack去哪些目录下寻找第三方模块。resolve.modules的默认值是[node modules],含义是先去当前目录的/node modules目录下去找我们想找的模块,如果没找到,就去上一级目录../node modules中找,再没有就去../ .. /node modules中找,以此类推,这和Node.js的模块寻找机制很相似。当安装的第三方模块都放在项目根目录的./node modules目录下时,就没有必要按照默认的方式去一层层地寻找,可以指明存放第三方模块的绝对路径,以减少寻找。
    优化后配置:

    // webpack.base.conf.js
    resolve: {
    // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
    modules: [resolve('src'), resolve('node_modules')]
    },
    function resolve (dir) {
       return path.join(__dirname, '..', dir)
    }
    
    优化resolve.alias配置

    resolve.alias配置项通过别名来将原导入路径映射成一个新的导入路径。
    优化后配置:

    alias: {
      '@': resolve('src'),
    },
    // 通过以上的配置,引用src底下的common.js文件,就可以直接这么写
    import common from '@/common.js';
    
    优化resolve.extensions配置

    在导入语句没带文件后缀时,Webpack 会在自动带上后缀后去尝试询问文件是否存在。默认是:extensions :[‘. js ‘,’. json ’] 。也就是说,当遇到import ( '. /data ’)这样的导入语句时,Webpack会先去寻找./data .js 文件,如果该文件不存在,就去寻找./data.json 文件,如果还是找不到就报错。如果这个列表越长,或者正确的后缀越往后,就会造成尝试的次数越多,所以 resolve .extensions 的配置也会影响到构建的性能。
    优化措施:

    • 后缀尝试列表要尽可能小,不要将项目中不可能存在的情况写到后缀尝试列表中。
    • 频率出现最高的文件后缀要优先放在最前面,以做到尽快退出寻找过程。
    • 在源码中写导入语句时,要尽可能带上后缀,从而可以避免寻找过程。例如在确定的情况下将 require(’. /data ’)写成require(’. /data.json ’)
    优化resolve.noParse配置

    noParse配置项可以让Webpack忽略对部分没采用模块化的文件的递归解析和处理,这样做的好处是能提高构建性能。原因是一些库如jQuery、lodash 庞大又没有采用模块化标准,让Webpack去解析这些文件既耗时又没有意义。
    noParse是可选的配置项,类型需要是RegExp 、[RegExp]、function中的一种。例如,若想要忽略jQuery 、ChartJS ,则优化配置如下:

    // webpack.base.conf.js
        module: {
            noParse: '/jquery|lodash/',  // 不去解析三方库
            rules: []
        }
    

    二、使用HappyPack多进程解析和处理文件

    由于有大量文件需要解析和处理,所以构建是文件读写和计算密集型的操作,特别是当文件数量变多后,Webpack构建慢的问题会显得更为严重。运行在 Node.之上的Webpack是单线程模型的,也就是说Webpack需要一个一个地处理任务,不能同时处理多个任务。Happy Pack ( https://github.com/amireh/happypack )就能让Webpack做到这一点,它将任务分解给多个子进程去并发执行,子进程处理完后再将结果发送给主进程。
    项目中HappyPack使用配置:

    (1)HappyPack插件安装:
        $ npm i -D happypack
    (2)webpack.base.conf.js 文件对module.rules进行配置
        module: {
         rules: [
          {
            test: /\.js$/,
            // 将对.js 文件的处理转交给 id 为 babel 的HappyPack实例
              use:['happypack/loader?id=babel'],
              include: [resolve('src'), resolve('test'),   
                resolve('node_modules/webpack-dev-server/client')],
            // 排除第三方插件
              exclude:path.resolve(__dirname,'node_modules'),
            },
            {
              test: /\.vue$/,
              use: ['happypack/loader?id=vue'],
            },
          ]
        },
    (3)webpack.prod.conf.js 文件进行配置   
        const HappyPack = require('happypack');
        // 构造出共享进程池,在进程池中包含5个子进程
        const HappyPackThreadPool = HappyPack.ThreadPool({size:5});
        plugins: [
           new HappyPack({
             // 用唯一的标识符id,来代表当前的HappyPack是用来处理一类特定的文件
             id:'vue',
             loaders:[
               {
                 loader:'vue-loader',
                 options: vueLoaderConfig
               }
             ],
             threadPool: HappyPackThreadPool,
           }),
    
           new HappyPack({
             // 用唯一的标识符id,来代表当前的HappyPack是用来处理一类特定的文件
             id:'babel',
             // 如何处理.js文件,用法和Loader配置中一样
             loaders:['babel-loader?cacheDirectory'],
             threadPool: HappyPackThreadPool,
           }),
        ]
    

    三、使用ParallelUglifyPlugin多进程压缩代码文件

    由于压缩JavaScript 代码时,需要先将代码解析成用 Object 抽象表示的 AST 语法树,再去应用各种规则分析和处理AST ,所以导致这个过程的计算量巨大,耗时非常多。当Webpack有多个JavaScript 文件需要输出和压缩时,原本会使用UglifyJS去一个一个压缩再输出,但是ParallelUglifyPlugin会开启多个子进程,将对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过UglifyJS去压缩代码,但是变成了并行执行。所以 ParallelUglify Plugin能更快地完成对多个文件的压缩工作。
    项目中ParallelUglifyPlugin使用配置:

    (1)ParallelUglifyPlugin插件安装:
         $ npm i -D webpack-parallel-uglify-plugin
    (2)webpack.prod.conf.js 文件进行配置
        const ParallelUglifyPlugin =require('webpack-parallel-uglify-plugin');
        plugins: [
        new ParallelUglifyPlugin({
          cacheDir: '.cache/',
          uglifyJs:{
            compress: {
              warnings: false
            },
            sourceMap: true
          }
         }),
        ]
    

    四、使用自动刷新

    借助自动化的手段,在监听到本地源码文件发生变化时,自动重新构建出可运行的代码后再控制浏览器刷新。Webpack将这些功能都内置了,并且提供了多种方案供我们选择。
    项目中自动刷新的配置:

    // webpack.dev.config.js
    module: {},
    devServer: {
      watchOptions: {
        // 不监听的文件或文件夹,支持正则匹配
        ignored: /node_modules/,
        // 监听到变化后等300ms再去执行动作
        aggregateTimeout: 300,
        // 默认每秒询问1000次
        poll: 1000
      }
    }
    

    **相关优化措施: **
    (1)配置忽略一些不监听的一些文件,如:node_modules。
    (2)watchOptions.aggregateTirneout 的值越大性能越好,因为这能降低重新构建的频率。
    (3) watchOptions.poll 的值越小越好,因为这能降低检查的频率。

    五、开启模块热替换

    DevServer 还支持一种叫做模块热替换( Hot Module Replacement )的技术可在不刷新整个网页的情况下做到超灵敏实时预览。原理是在一个源码发生变化时,只需重新编译发生变化的模块,再用新输出的模块替换掉浏览器中对应的老模块 。模块热替换技术在很大程度上提升了开发效率和体验 。
    项目中模块热替换的配置:

    // webpack.dev.config.js
    devServer: {
      hot: true,
    },
    plugins: [
      new webpack.HotModuleReplacementPlugin(),
    // 显示被替换模块的名称
      new webpack.NamedModulesPlugin(), // HMR shows correct file names
    ]
    

    六、提取公共代码

    如果项目中没有去将每个页面的第三方库和公共模块提取出来,则项目会存在以下问题:

    • 相同的资源被重复加载,浪费用户的流量和服务器的成本。
    • 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。

    所以我们需要将多个页面的公共代码抽离成单独的文件,来优化以上问题 。Webpack 内置了专门用于提取多个Chunk 中的公共部分的插件 CommonsChunkPlugin,我们在项目中 CommonsChunkPlugin 的配置如下:

    // webpack.prod.config.js
    // 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这个文件中。
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: function(module, count) {
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        );
      }
    }),
    // 抽取出代码模块的映射关系
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      chunks: ['vendor']
    })
    

    七、资源压缩

    资源压缩包括图片、HTML、JS、CSS文件压缩,作用是减小打包体积,加载速度更快,优化首屏加载速度和切换页面速度。

    • 图片压缩插件:image-webpack-loader
    • HTML压缩插件:html-webpack-plugin
    • JS压缩插件:uglifyjs-webpack-plugin
    • CSS压缩插件:optimize-css-assets-webpack-plugin

    (1)首先安装image-webpack-loader

    npm install image-webpack-loader --save-dev
    

    (2)然后在webpack.base.conf.js中进行配置

    {
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      use:[
        {
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('img/[name].[hash:7].[ext]')
          }
        },
        {
          loader: 'image-webpack-loader',
          options: {
            bypassOnDebug: true,
          }
        }
      ]
    }
    

    八、优化SourceMap

    简单说,Source map就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。
    有了它,出错的时候,出错工具将直接显示原始代码,而不是转换后的代码。这无疑给开发者带来了很大方便。
    在webpack的development模式下,会自动开启source-map,它会精准捕获报错代码所处的文件、行数,这是source-map最重要的作用,在生产环境是自动关闭
    sourceMap会将业务代码全部显示出来会有安全问题,所以生产环境需要关闭。

    我们在项目进行打包后,会将开发中的多个文件代码打包到一个文件中,并且经过压缩、去掉多余的空格、babel编译化后,最终将编译得到的代码会用于线上环境,那么这样处理后的代码和源代码会有很大的差别,当有 bug的时候,我们只能定位到压缩处理后的代码位置,无法定位到开发环境中的代码,对于开发来说不好调式定位问题,因此 sourceMap 出现了,它就是为了解决不好调式代码问题的。
    SourceMap 的可选值如下(+ 号越多,代表速度越快,- 号越多,代表速度越慢, o 代表中等速度 )


    开发环境推荐: cheap-module-eval-source-map
    生产环境推荐: cheap-module-source-map
    原因如下:
    • cheap: 源代码中的列信息是没有任何作用,因此我们打包后的文件不希望包含列相关信息,只有行信息能建立打包前后的依赖关系。因此不管是开发环境或生产环境,我们都希望添加 cheap 的基本类型来忽略打包前后的列信息;
    • module :不管是开发环境还是正式环境,我们都希望能定位到bug的源代码具体的位置,比如说某个 Vue 文件报错了,我们希望能定位到具体的 Vue 文件,因此我们也需要 module 配置;
    • soure-map :source-map 会为每一个打包后的模块生成独立的 soucemap 文件 ,因此我们需要增加source-map 属性;
    • eval-source-map:eval 打包代码的速度非常快,因为它不生成 map 文件,但是可以对 eval 组合使用 eval-source-map 使用会将 map 文件以 DataURL 的形式存在打包后的 js 文件中。在正式环境中不要使用 eval-source-map, 因为它会增加文件的大小,但是在开发环境中,可以试用下,因为他们打包的速度很快。

    九、构建结果输出分析

    Webpack输出的代码可读性非常差而且文件非常大,让我们非常头疼。为了更简单、直观地分析输出结果,社区中出现了许多可视化分析工具。这些工具以图形的方式将结果更直观地展示出来,让我们快速了解问题所在。接下来讲解vue项目中用到的分析工具:webpack-bundle-analyzer 。

    先在项目里面安装这个 plugin

    npm install --save-dev webpack-bundle-analyzer
    

    在webpack.prod.conf.js中添加

    const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
    module.exports = { 
      plugins: [
         new BundleAnalyzerPlugin()
      ]
    };
    

    最后执行执行 npm run build --report 后生成分析报告如下:


    在这个树形图片里,会有包含下面的内容:

    • 每个打包以后的 bundle 文件里面,真正包含哪些内容,项目里的 module、js、component、html、css、img 最后都被放到哪个对应的 bunlde 文件里了。
    • 每个 bundle 文件里,列出了每一个的 module、componet、js 具体 size,同时会列出 start size、parsed size、gzip size 这三种不同的形式下到底多大,方便优化。

    start size:原始没有经过 minify 处理的文件大小
    parse size:比如 webpack plugin 里用了 uglify,就是 minified 以后的文件大小
    gzip size:被压缩以后的文件大小

    基于以上给出的信息, 你就能比较直观的在图片里看到,哪些公用 library 被重复打包到不同的 bundle 文件里,或者是说哪一个过大影响性等等;从而你就可以对你的 webpack 打包方式进行优化。

    相关文章

      网友评论

        本文标题:Webpack优化实践

        本文链接:https://www.haomeiwen.com/subject/cppsiltx.html