• home > tools > Bundler > webpack >

    webpack性能优化(0):webpack性能优化概况-优化构建速度

    Date:

    webpack打包优化的重点回顾,一些零星知识的整理,也欢迎大家补充。

    优化构建速度的目的

    当我们的应用还处于小规模的时候,我们可能不会在乎Webpack的编译速度,无论使用3.X还是4.X版本,它都足够快,或者说至少没让你等得不耐烦。但随着业务的增多,嗖嗖嗖一下项目就有上百个组件了,也是件很简单的事情。这时候当你再独立编前端模块的生产包时,或者CI工具中编整个项目的包时,如果Webpackp配置没经过优化,那编译速度都会慢得一塌糊涂。编译耗时十多秒的和编译耗时一两分钟的体验是迥然不同的。从开发期间做临时部署时的效率以及CI的整体效率这两点出发,我们都有必要去加快前端项目的编译速度,这是对前端开发工作效率上的提升。

    减少目录检索范围

    Webpack在启动后会根据Entry配置的入口出发,递归地解析所依赖的文件。这个过程分为搜索文件和把匹配的文件进行分析、转化的两个过程,因此可以从这两个角度来进行优化配置。

    1)resolve字段告诉webpack怎么去搜索文件,所以首先要重视resolve字段的配置

    • 设置resolve.modules:[path.resolve(__dirname, 'node_modules')]避免层层查找

      resolve.modules告诉webpack去哪些目录下寻找第三方模块,默认值为['node_modules'],会依次查找./node_modules、../node_modules、../../node_modules

    • 设置resolve.mainFields:['main'],设置尽量少的值可以减少入口文件的搜索步骤

      第三方模块为了适应不同的使用环境,会定义多个入口文件,mainFields定义使用第三方模块的哪个入口文件,由于大多数第三方模块都使用main字段描述入口文件的位置,所以可以设置单独一个main值,减少搜索

    • 对庞大的第三方模块设置resolve.alias, 使webpack直接使用库的min文件,避免库内解析

      如对于react:resolve.alias:{'react':patch.resolve(__dirname, './node_modules/react/dist/react.min.js')}

      这样会影响Tree-Shaking,适合对整体性比较强的库使用,如果是像lodash这类工具类的比较分散的库,比较适合Tree-Shaking,避免使用这种方式。

    • 合理配置resolve.extensions,减少文件查找

      默认值:extensions:['.js', '.json'],当导入语句没带文件后缀时,Webpack会根据extensions定义的后缀列表进行文件查找,所以:

      1. 列表值尽量少

      2. 频率高的文件类型的后缀写在前面

      3. 源码中的导入语句尽可能的写上文件后缀,如require(./data)要写成require(./data.json)

    resolve.alias 可以配置 webpack 模块解析的别名,对于比较深的解析路径,可以对其配置 alias. 可以提升 webpack 的构建速度。

    alias: {
        Utilities: path.resolve(__dirname, 'src/utilities/'),
        Templates:path.resolve(__dirname, 'src/templates/')
    }

    在实际项目开发过程中,我们并不需要实时调试各种库的源码,这时候就可以考虑使用external选项

    2)module.noParse字段告诉Webpack不必解析哪些文件,可以用来排除对非模块化库文件的解析

    如jQuery、ChartJS,另外如果使用resolve.alias配置了react.min.js,则也应该排除解析,因为react.min.js经过构建,已经是可以直接运行在浏览器的、非模块化的文件了。noParse值可以是RegExp、[RegExp]、function

    module:{ noParse:[/jquery|chartjs/, /react\.min\.js$/] }

    3)配置loader时,通过test、exclude、include缩小搜索范围,减少 loader 遍历的目录范围,从而加快 Webpack 编译速度。

    比如指定 babel-loader 只处理业务代码:

    { test: /\.js$/,use: ['babel-loader'],include: path.join(__dirname, 'app')}

    推荐阅读《webpack之前端性能优化

    配置externals

    如果需要引用一个库,但是又想让webpack打包(减少打包的时间),并且又不影响我们在程序中以CMD、AMD或者window/global全局等方式进行使用(一般都以import方式引用使用),那就可以通过配置externals。

    webpack 中的 externals 配置提供了不从 bundle 中引用依赖的方式。解决的是,所创建的 bundle 依赖于那些存在于用户环境(consumer environment)中的依赖。

    这样做的目的就是将不怎么需要更新的第三方库脱离webpack打包,不被打入bundle中,从而减少打包时间,但又不影响运用第三方库的方式,例如import方式等。

    externals支持模块上下文的方式

    • global - 外部 library 能够作为全局变量使用。用户可以通过在 script 标签中引入来实现。这是 externals 的默认设置。

    • commonjs - 用户(consumer)应用程序可能使用 CommonJS 模块系统,因此外部 library 应该使用 CommonJS 模块系统,并且应该是一个 CommonJS 模块。

    • commonjs2 - 类似上面几行,但导出的是 module.exports.default。

    • amd - 类似上面几行,但使用 AMD 模块系统。

    怎么运用externals

    在index.html中引入CDN的资源(如:react全家桶之类的资源)

    <script src="https://cdn.zhoulujun.net/react.vendor.js"></script>
    <script src="https://cdn.zhoulujun.net/react.dom.js"></script>

    webpack.config.js配置如下

    module.exports = {
         ...
         output: {
           ...
         },
         externals : {
           react: 'react',
           moment: 'moment'
         }
    }

    这样的话在应用程序中依旧可以以import的方式(还支持其他方式)引用

    不仅之前对第三方库的用法方式不变,还把第三方库剥离出webpack的打包中,从而加速webpack的打包速度。

    externals和libraryTarget的关系

    • libraryTarget配置如何暴露 library。如果不设置library,那这个library就不暴露。就相当于一个自执行函数

    • externals是决定的是以哪种模式去加载所引入的额外的包

    • libraryTarget决定了你的library运行在哪个环境,哪个环境也就决定了你哪种模式去加载所引入的额外的包。也就是说,externals应该和libraryTarget保持一致。library运行在浏览器中的,你设置externals的模式为commonjs,那代码肯定就运行不了了。

    • 如果是应用程序开发,一般是运行在浏览器环境libraryTarget可以不设置,externals默认的模式是global,也就是以全局变量的模式加载所引入外部的库。

    缓存编译

    一般配置如下

    • 通过babel-loader的cache配置来缓存babel的编译结果。

    • 通过terser-webpack-plugin的parallel和cache配置来并行处理并缓存之前的编译结果。

    • 使用 cache-loader 启用持久化缓存。

    利用 DllPlugin 和 DllReferencePlugin 预编译资源模块

    把改变频率比较小的第三方库等依赖单独打包构建,在打包整个项目的时候,如果解析到了通过 Dll 形式进行打包的依赖,会在正常的打包过程中跳过,同时把对这些依赖的引入导入到 Dll 模块上去。 这样会大大提升在对业务代码进行打包时候的速度。

    • 使用DllPlugin配置一个webpack_dll.config.js来构建dll文件:

    • 在主config文件里使用DllReferencePlugin插件引入xx.manifest.json文件:

    相对于externals,dllPlugin有如下几点优势:

    1. dll预编译出来的模块可以作为静态资源链接库可被重复使用,尤其适合多个项目之间的资源共享,如同一个站点pc和手机版等;

    2. dll资源能有效地解决资源循环依赖的问题,部分依赖库如:react-addons-css-transition-group这种原先从react核心库中抽取的资源包,整个代码只有一句话:

      module.exports = require('react/lib/ReactCSSTransitionGroup');

    3. 却因为重新指向了react/lib中,这也会导致在通过externals引入的资源只能识别react,寻址解析react/lib则会出现无法被正确索引的情况。

    4. 由于externals的配置项需要对每个依赖库进行逐个定制,所以每次增加一个组件都需要手动修改,略微繁琐,而通过dllPlugin则能完全通过配置读取,减少维护的成本;

    利用多线程优化编译速度

    webpack中为了方便各种资源和类型的加载,设计了以loader加载器的形式读取资源,但是受限于node的编程模型影响,所有的loader虽然以async的形式来并发调用,但是还是运行在单个 node的进程以及在同一个事件循环中,这就直接导致了当我们需要同时读取多个loader文件资源时,比如babel-loader需要transform各种jsx,es6的资源文件。在这种同步计算同时需要大量耗费cpu运算的过程中,node的单进程模型就无优势了,那么happypack就针对解决此类问题而生。 

    • 开启happypack的线程池——推荐阅读《happypack 原理解析

    • 使用ParallelUglifyPlugin开启多进程压缩JS文件

    自动刷新优化

    Webpack可以使用两种方式开启监听:1. 启动webpack时加上--watch参数;2. 在配置文件中设置watch:true。此外还有如下配置参数。合理设置watchOptions可以优化监听体验。

    module.exports = {

        watch: true,

        watchOptions: {

            ignored: /node_modules/,//设置不监听的目录,排除node_modules后可以显著减少Webpack消耗的内存

            aggregateTimeout: 300,  //件变动后多久发起构建,避免文件更新太快而造成的频繁编译以至卡死,越大越好

            poll: 1000,  //通过向系统轮询文件是否变化来判断文件是否改变,poll为每秒询问次数,越小越好

        }

    }

    DevServer刷新浏览器有两种方式:

    • 向网页中注入代理客户端代码,通过客户端发起刷新

    • 向网页装入一个iframe,通过刷新iframe实现刷新效果


    开启模块热替换HMR

    模块热替换不刷新整个网页而只重新编译发生变化的模块,并用新模块替换老模块,所以预览反应更快,等待时间更少,同时不刷新页面能保留当前网页的运行状态。原理也是向每一个chunk中注入代理客户端来连接DevServer和网页。开启方式:

    webpack-dev-server --hot

    使用HotModuleReplacementPlugin,比较麻烦

    开启后如果修改子模块就可以实现局部刷新,但如果修改的是根JS文件,会整页刷新,原因在于,子模块更新时,事件一层层向上传递,直到某层的文件接收了当前变化的模块,然后执行回调函数。如果一层层向外抛直到最外层都没有文件接收,就会刷新整页。

    使用 NamedModulesPlugin 可以使控制台打印出被替换的模块的名称而非数字ID,另外同webpack监听,忽略node_modules目录的文件可以提升性能。

    Build Cache

    Webpack 和一些 Plugin/Loader 都有 Cache 选项。开启 Cache 选项,有利用提高构建性能。

    比如:使用 babel-loader 的时候开启 cacheDirectory 选项,会较为明显的提升构建速度

    选择合适的 Devtool 版本

    webpack 的 devtool 配置,决定了在构建过程中怎样生成 sourceMap 文件。通常来说eval的性能最高,但是不能生成的 sourceMap 文件解析出来的代码,和源代码差异较大。 source-map 的性能较差,但是可以生成原始版本的代码。 在大多数 Development 场景下 cheap-module-eval-source-map 是最佳的选择。

    合理配置 CommonsChunkPlugin

    webpack的资源入口通常是以entry为单元进行编译提取,那么当多entry共存的时候,CommonsChunkPlugin的作用就会发挥出来,对所有依赖的chunk进行公共部分的提取,但是在这里可能很多人会误认为抽取公共部分指的是能抽取某个代码片段,其实并非如此,它是以module为单位进行提取。


    假设我们的页面中存在entry1,entry2,entry3三个入口,这些入口中可能都会引用如utils,loadash,fetch等这些通用模块,那么就可以考虑对这部分的共用部分机提取。通常提取方式有如下四种实现:

    1、传入字符串参数,由chunkplugin自动计算提取

    new webpack.optimize.CommonsChunkPlugin('common.js')

    这种做法默认会把所有入口节点的公共代码提取出来, 生成一个common.js

    2、有选择的提取公共代码

    new webpack.optimize.CommonsChunkPlugin('common.js',['entry1','entry2']);

    只提取entry1节点和entry2中的共用部分模块, 生成一个common.js

    3、将entry下所有的模块的公共部分(可指定引用次数)提取到一个通用的chunk中

    new webpack.optimize.CommonsChunkPlugin({

        name: 'vendors',

        minChunks: function (module, count) {

           return (

              module.resource &&

              /\.js$/.test(module.resource) &&

              module.resource.indexOf(

                path.join(__dirname, '../node_modules')

              ) === 0

           )

        }

    });

    提取所有node_modules中的模块至vendors中,也可以指定minChunks中的最小引用数;

    4、抽取enry中的一些lib抽取到vendors中

    entry = {

        vendors: ['fetch', 'loadash']

    };

    new webpack.optimize.CommonsChunkPlugin({

        name: "vendors",

        minChunks: Infinity

    });

    添加一个entry名叫为vendors,并把vendors设置为所需要的资源库,CommonsChunk会自动提取指定库至vendors中。

    配置profile:true,用于分析是什么原因导致构建性能不佳

    其次,使用 webpack-visualizerwebpack-bundle-analyzer进行分析

    推荐阅读《三十分钟掌握Webpack性能优化

    使用Scope Hoisting

    通过分析模块间的依赖关系,尽可能将被打散的模块合并到一个函数中,但不能造成代码冗余,所以只有被引用一次的模块才能被合并。由于需要分析模块间的依赖关系,所以源码必须是采用了ES6模块化的,否则Webpack会降级处理不采用Scope Hoisting。

    使用方法

    const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');

    //...

    plugins:[

        new ModuleConcatenationPlugin();

    ]

    拆分页面

    webpack4不仅可以生成单个html文件,也可以生成多个,并且给每个html文件配置不同的JS,具体配置如下:

     plugins: [

            new HtmlWebpackPlugin({

                filename : 'index.html', //生成的文件名称

                chunks : ['index'], //加入的js文件,若无此属性,则默认为所有js

                hash : true, //生成hash数值,避免产生缓存

                title : '实际标题', //html的title标签值

                template : './src/index.html' //模板文件路径

            }),

            new HtmlWebpackPlugin({

                filename : 'main.html',

                hash : true,

                title : '实际标题',

                template : './src/index.html'

            })

        ]


    参考文章:

    优化Webpack构建性能的几点建议

    webpack 构建性能优化策略小结




    转载本站文章《webpack性能优化(0):webpack性能优化概况-优化构建速度》,
    请注明出处:https://www.zhoulujun.net/html/tools/Bundler/webpack/2016_0218_7492.html