美文网首页前端小库
一文搞懂webpack打包优化方案

一文搞懂webpack打包优化方案

作者: 喜剧之王爱创作 | 来源:发表于2020-09-09 15:42 被阅读0次
    webpackhot.jpg

    写在前面

    在现在前端工程化的大背景下,webpack成为了最常用的打包工具之一,有一社区或者优秀团队,也都以Webpack为基础构建自己的脚手架,比如我们所熟知对的vue-cli,umijs等,通常情况下,这些脚手架多多少少会为我们配置好一些关于打包优化的东西,如果你的项目并不复杂,可能很长一段时间你都无法感知打包优化的重要性,如果忽然遇到了打包优化的问题,可能太过让人措手不及,不管你使用的是社区优秀的脚手架,还是自己基于webpack搭建的项目或者脚手架,搞懂webpack打包优化,会让我拥有解决更多高级问题的能力,也会让你的项目更加“丝滑”。

    webpack打包优化

    打包优化主要从两个方面下手

    • 打包速度,优化打包速度,主要是提升了我们的开发效率,更快的打包构建过程,将让你保持一颗愉悦的心
    • 打包大小,优化打包体积,主要是提升产品的使用体验,降低服务器资源成本,更快的页面加载,将让产品显得更加“丝滑”,同时也可以让打包更快
    打包速度优化

    当我们做一些较大型项目的打包时,经常会遇到,打包时间过长的
    问题,让人焦急不已,那么我们就要采用一些手段来提升webpack的打包。

    跟上技术的迭代(webapck,Node, Npm)

    如果想要提升打包的速度,将打包技术生态中涉及的技术版本更新将是一个最简单的方式,那么为什么更新版本会提升打包速度呢?
    Webpack的每次更新,必然会更新底层的一些打包原理和api来提升打包速度,更新Webpack版本将有助于提升打包速度,同事,webpack又是运行在Node环境下,如果Node版本提升,其运行效率也会提升,那么webpack运行在node之上也会有所提升的,同样,我们使用更新的Npm或者Yarn的包管理工具的话,新的包管理工具会更快的帮我们分析一下包的依赖或者包的引入,这样也会间接的提升webpack的打包速度。

    在尽可能少的模块上使用Loader
    { 
        test: /\.js$/,
        loader: 'babel-loader',
    }
    

    看上面的代码,是我们在配置bable-loader时的代码,如果这样配置的话,那么整个项目的js文件,都会做babel-loader的转译,但实际上,node_modules中的包都是帮我们转译过的,重复的转译,势必会降低webapck的打包速度,这时候我们就要通过设置babel-loader的作用范围来提升打包速度。

    { 
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
    }
    

    通过上面的配置,我们就不用再对node_modules中js文件做转译了,当然了,除了exclude选项排除某个范围,我们还可以通过include选项去指定某个范围,比如上面的代码也可以改成

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

    所以,我们可以通过合理的使用exclude或者include这样的配置项,去指定某一个loader的执行范围,从而降低了loader的执行频率,loader的编译过程被少量的执行了,那么webpack的打包速度自然也会得到提升。
    不光babel-loader,其他loader也是可以通过具体的项目分析,做这样的配置的。

    将babel编译过的文件缓存起来

    babel-loader为我们提供了cacheDirectory参数,可以参考官网对其做相应配置

    Plugin尽可能精简并且可靠

    我们应该尽可能少的使用Plugin,并且还要保证其可靠性,举个栗子。
    我们在生产环境下的打包一般会需要通过MiniCssExtractPluginOptimizeCSSAssetsPlugin两个插件来做样式代码的分离或者压缩,这也是十分必要的,当然,如果你在本地环境下使用了CSS代码的分离压缩,不但没有必要(因为本地代码只有自己看,也不去在意其是否压缩),反而会降低打包的效率,因为Webpack插件是基于webpack打包过程事件流的,没一个插件的执行,都会消耗性能,降低效率,所以,如果非必要,就不要去使用一些插件了,如果你很有必要去使用某个插件,那么最好是使用Webapck官网提供的插件,因为官方的插件是经过一些专门的性能测试的,相对于第三方的插件来说,性能会高一些,而第三方的插件,很有可能性能得不到保证,降低你的打包速度,所以,在使用一个插件之前,一定要做好选择哦!

    resolve参数合理配置
    1. extensions

    resolve参数是一个webpack配置项,我们先开介绍一下这个配置项的使用,比如现在有下面的文件目录

    |--src
      |--index.js
      |--child.jsx
    

    我们想要在index.js中使用child.jsx可以这样使用

    import Child from './child.jsx'
    

    但是我们可以通过配置resolve选项,来达到下面这样的引用方式

    import Child from './child'
    

    如下:

    module.exports = {
        resolve: {
            extensions: ['.js', '.jsx']
        },
    }
    

    上面的意思是,我们遇到'./child'这样的字段后,会去当前目录下查找'js'后缀的文件,没有找到再去查找'jsx'后缀的文件,这样我们就可以省去在引用的过程中写前缀了,但是,有些同学可能会不合理的配置resolve,比如

    module.exports = {
        resolve: {
            extensions: ['css','jpg','.js', '.jsx']
        },
    }
    

    如果像上面这样配置,那么在你引入一个文件的时候,就会按照上面的裂变挨个的去查找,实际上,这样是有性能损耗的
    所以,一般情况下,我们只有遇到js或者jsx或者vue等等这样逻辑型文件的时候才去配置到resolve中,像css这样的文件就不去配置了这样,不但开发起来方便一些,同事性能上也会得到一些平衡。

    1. mainFiles

    在平时开发中大家一定也遇到过这样的引用

    import Child from './components/'
    

    这时候,会自动找到'components'文件夹下的'index.js'文件,假如我们现在的文件目录如下

    |--src
      |--components
        |--child.jsx
      |--index.jsx
    

    我们在index.jsx中想要通过

    import Child from './components/'
    

    上面这种引用方式引入'components'下的‘child.jsx’文件,那么我们可以做下面这样的配置

    module.exports = {
      resolve: {
          extensions: ['.js', '.jsx'],
          +++  mainFiles: ['index', 'child']
      },
    }
    

    这样,我们在引用一个文件夹时,他就会默认去找下面的index.js找不到再去找child.js了。
    但是,这样又会带来性能问题,通过上面的配置后,每次我们引入一个路径的话,都会去做一遍文件的匹配,所以我们要根据自己的需要,平衡好性能和开发方便后再做相应的配置,一般来说,我们不需要配置这个项

    1. alias

    在一些社区脚手架中,我们还会见到下面这样的引用方式

    import Child from '@/component/'
    

    其配置如下

    module.exports = {
        resolve: {
            extensions: ['.js', '.jsx'],
            +++ alias: {
                '@': path.resolve(__dirname, '../src')
            }
        },
    }
    

    意思是,我们用‘@’代替了根目录下的src目录,这样你会在开发的时候提升一些开发效率。同样,他也会带来一些性能上的问题,所以,大家依然需要平衡好开发效率和打包效率,有针对性的去使用
    通过上面,举了三个栗子,说明了resolve配置项对于开发效率的提升帮助,同事他也具有一点的性能问题,大家在使用的过程中,要在做好平衡,按照需要去做相应的配置。

    使用DllPlugin提高打包速度

    我先对我手上一个简单的项目做个打包,记录下打包时间如下

    buildtime99.png

    基本时间稳定在1500ms,我们暂认定当前情况下的打包速度为1300ms,我的代码现在是这样的

    import React from 'react'
    import ReactDom from 'react-dom'
    import _ from 'lodash'
    
    const App = () => {
        return (
            <div>
                <div>{_.join(['hello','world'], ' ')}</div>
            </div>
        )
    }
    ReactDom.render(<App/>, document.getElementById('root'))
    

    其中像react,react-dom,lodash这样的库,是基本不会改变的,但是现在,我们每一次打包都要对其进行分析,都要消耗一定的时间,于是我们就想,可以把第三方库单独打包为一个文件,只在第一次打包的时候做分析,后面就使用第一次打包的结果这样就可以提高打包速度了,我们以这个为思路,展开这次的优化。

    1. 配置第三方库单独打包

    我们再创建一个webpack.dll.js的配置文件,内容如下

    const path = require('path')
    module.exports = {
        mode: 'production',
        entry: {
            vendors: ['react', 'react-dom', 'lodash']
        },
        output: {
            filename: '[name].dll.js',
            path: path.resolve(__dirname, '../dll'),
            library: '[name]'
        }
    }
    

    上面的意思是,我们将几个第三方库做单独的打包,并以Library的形式导出,这时候会在根目录下生成一个'dll'的文件。我们期望将该文件在最终生成的index.html中以全局变量的形式引入。所以还需要在原有的打包配置中,配置一个插件,来动态的引入我们生成的第三方库,因为现在的第三方库是以Library的形式存在于项目中,并以一个‘vendors’变量全局暴露。这样我们就可以以全局变量的形式访问第三方库

    1. 配置add-asset-html-webpack-plugin

    我们安装这个webapck插件,并配置如下

    module.exports = {
      plugins: [
            new AddAssetHtmlWebpackPlugin({
                filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
            })
      ]
    }
    

    意思是我们通过上面这个插件,就可以为生成的index.html引入我们单独打包的第三方库,配置成功后,启动项目你会发现源码中已经引入‘vendors.dll.js’了。

    html.png
    并且在也可以全局访问一个‘vendors’变量(因为我们是以Library的形式打包,并暴露出一个vendors变量)
    到这里,我们实现了一个第三方模块只打包一次的目标,但是现在还不能满足我们最初的,‘第三方模块只打包一次,且以后每次都使用’的目标,现在我们的项目中,其中还是使用的'node_modules'里面的内容,那么怎么才能让业务代码使用我们处理过的第三方模块呢?
    1. 使用Dllplugin做分析

    我们使用Dllplugin生成一个映射,操作如下
    对webpack.dll.js做下修改

    const path = require('path')
    const webpack = require('webpack')
    module.exports = {
        mode: 'production',
        entry: {
            vendors: ['react', 'react-dom', 'lodash']
        },
        output: {
            filename: '[name].dll.js',
            path: path.resolve(__dirname, '../dll'),
            library: '[name]'
        },
        plugins: [
            new webpack.DllPlugin({
                name: '[name]',
                path: path.resolve(__dirname, '../dll/[name].manifest.json')
            })
        ]
    }
    

    我们配置一个Dllplugin插件,需要注意的是DllPlugin中的name属性,一定要个output中的library属性一致,意思是,我们要对生成的library做一个分析分析的结果放到dll下的‘vendors.manifest.json’中。这时候再运行dll打包,就会看到这个'vendors.manifest.json'文件了。
    到这里我们想利用上面生成的全局变量,和现在生成的映射文件,我们是否可以实现在业务代码中,如果发现引用的模块是来自我们处理过的第三方模块,就使用我们已经打包过的包,反之才从node_modules中取

    1. 配置DllReferencePlugin

    要想实现上面的设想,我们还需要在打包配置文件中,做DllReferencePlugin插件的配置

    module.exports = {
      plugins: [
            new webpack.DllReferencePlugin({
                manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
            })
       ]
    }
    

    做了上面的配置后,我们打包时的原理变成了这样:在打包时,当遇到第三方模块,他会去到映射文件中去找是否存在于我们单独打包的第三方库中,如果存在,就从上面操作中暴露的全局变量中取,如果不存在,才从node_moudules中取,这时候,我们做一下打包时间对比


    dllplugin.png

    时间变成了900多毫秒,可以把上面的配置注释掉,再去看一下打包时间


    nodll.png

    时间又变成了1400多毫秒,由此可见,使用DllPlugin对于性能的提升还是很明显的。
    这个配置项讲的有点绕,下面针对这个插件的配置,我们做个小总结

    • 通过dll配置文件单独将第三方库打包为一个library形式,暴露一个全局变量出来
    • 通过DllPlugin插件,对打包文件做一个分析,生成一个映射文件
    • 在项目打包配置文件中,配置AddAssetHtmlWebpackPluginDllReferencePlugin,将映射关系引入进index.html中

    主要操作就是上面的三点了。下面我再对这个插件做一点扩展,上面我们是把三个第三方模块都打包到了,其实我们可以分开打包

    module.exports = {
        entry: {
            lodash: ['lodash'],
            react: ['react', 'react-dom']
        },
    }
    

    分开后,自然生成的library文件不一样了,映射文件也不一样了,所以我们还得再业务打包文件中做出更改

    module.exports = {
      plugins: [
            new AddAssetHtmlWebpackPlugin({
                filepath: path.resolve(__dirname, '../dll/lodash.dll.js')
            }),
            new AddAssetHtmlWebpackPlugin({
                filepath: path.resolve(__dirname, '../dll/react.dll.js')
            }),
            new webpack.DllReferencePlugin({
                manifest: path.resolve(__dirname, '../dll/lodash.manifest.json')
            }),
            new webpack.DllReferencePlugin({
                manifest: path.resolve(__dirname, '../dll/react.manifest.json')
            })
      ]
    }
    

    大家一定也发现了,其实这样的配置看起来是很臃肿的,于是我们可以这样修改我们的配置

    const plugins = [ // 定义一个数组,将基础的插件写入
        new HtmlWebpackPlugin({
            template: 'src/index.html'
        }),
        new CleanWebpackPlugin(),
        new webpack.ProvidePlugin({
            $: 'jquery'
        })
    ]
    // 利用NodeJs文件模块,分析dll文件夹下的文件,并动态插入
    const files = fs.readdirSync(path.resolve(__dirname, '../dll'))
    console.log(files) // 可以在这里查看结果感受一下
    files.forEach(file => {
        if(/.*\.dll.js/.test(file)) {
            plugins.push(
                new AddAssetHtmlWebpackPlugin({
                    filepath: path.resolve(__dirname, '../dll', file)
                })            
            )
        }
        if(/.*\.manifest.json/.test(file)) {
            plugins.push(
                new webpack.DllReferencePlugin({
                    manifest: path.resolve(__dirname, '../dll', file)
                })           
            )
        }
    })
    

    这样我们就不用手写的,如果你的dll于变动,只需要重新打包dll即可,不用再手动修改插件了。

    thread-loader和happypack

    因为受限于Node的单线程运行,所以webpack的打包也是单线程的,使用HappyPack可以将Loader的同步执行转为并行,从而执行Loader时的编译等待时间
    同时也可以使用webpack4官网提供的thread-loader来对有些耗时的loader做相应的处理,这里我将不再带大家熟悉其API,可以到对应的官网去参照其使用方法。

    合理使用Source Map

    Source Map为我们打包后的代码和源码提供了一种个映射关系,但是Source Map也会造成一些性能的问题,为了同时兼顾打包性能和开发调试方便,请使用合理的Source Map配置,这里可以参考我之前关于Source Map的讲解SourceMap配置

    开发环境内存编译

    我们知道,我们在本地的项目中,一般使用dev Server在本地起一个服务,而使用dev Server是不需要将dist文件打包进硬盘的,而是打包进内存里,从内存里读取文件的速度肯定是比硬盘快的多的,因为平时大家有意无意的已经这么实践,这里还是要提一下,知道其中的优化点

    开发环境无用插件剔除

    有些Webpack插件是针对于线上打包模式的,比如代码压缩,比如CSS分离压缩等,但是如果你在本地环境使用了这样的插件,将降低你的打包速度,同时有些插件在本地模式下使用,也是没有意义的,比如代码压缩。

    降低打包体积

    降低打包体积,不仅可以让打包后的项目运行更快,还可以对打包速度有所提升。我在下面将做详细的介绍

    上面为大家介绍了几种提升打包速度的方法,用来优化我们本地开发的效率,其中将到的DllPlugin也是内容比较多,需要主要的是,这个插件仅在开发环境下生效,并且在开发中,随着后续weebpack版本的更新,可能会引入一些缓存机制,到时候DllPlugin就不再使用了,这里我们大篇幅介绍他,希望大家能认识到并熟悉这种方式,用不用看大家


    打包大小优化

    上面提到,打包大小的优化主要对于产品的体验有很大的提升,那么我们有哪些手段可以控制打包的大小,从而让产品运行很流畅呢?

    tree shaking

    我们知道,webapck4默认在production模式下开启tree Shaking,用来删除调那些无效的引入,从而减小打包代码的体积,当然你也可以尝试在本地模式下配置,不过没啥太实际的作用,具体可参考我之前关于Tree Shaking的讲解文章。

    代码压缩

    webpack4在production模式下默认开启代码压缩。这一点大家要知道

    代码分割

    我们可以使用代码分割,将固定不变的一些代码如node_moudles中的代码单独打包,从而降低main.js的大小,利用浏览器的缓存机制,提高首屏加载的速度。具体的代码,可以看我之前关于Split code的讲解文章文章一文章二

    按需加载

    按需加载,也是个比较大的概念了,我举几个常见的按需加载场景。

    1. polyfill按需加载

    我们知道,polyfill实际是一种webpack shaming方案,如果我们不做处理,将是全量的引入所有的转译语法,但实际项目中,我们不一定都用的到,这时候需要做一下按需加载的配置,可以配置@babel/preset-envuseBuiltIns:usage,具体的内容可以参考我之前关于babel的文章讲解babel

    1. UI组件库的按需加载

    现在社区的大部分组件都是支持按需加载配置,或者tree shaking的,这样我们就不需要将整个UI库引入了,因为你可能项目中用不到所有的,具体我们可以参考babel-plugin-import的使用方法,或者组件库推荐的按需加载方案

    1. 路由按需加载

    路由的按需加载也叫路由懒加载,也就是,只有当我们访问到该页面时,才加载该页面的资源,这个方案其实不影响打包大小,算是一种代码分割的方案,我们通过异步的加载路由下对应的组件资源,利用代码分割单独打包。这里可以自己去看一下,不同的框架对应的路由懒加载方案

    写在后面

    本文用很大的篇幅介绍了Webapck的性能优化,尽量避免知识点过散,不利于总结,其实,关于webpack的打包优化方案,还有好多,甚至到webpack5的时候,webpack的打包性能又会优化不少,像上面提到的DllPlugiin可能将不再使用,随着技术的更新,Webpack优化的手段也将越来越丰富,大家可以根据自己的需要去拓展更多的优化手段。

    相关文章

      网友评论

        本文标题:一文搞懂webpack打包优化方案

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