您的位置:新葡亰496net > 服务器网络 > 0打包优化计策整理小结_javascript技术_脚本之家,

0打包优化计策整理小结_javascript技术_脚本之家,

发布时间:2019-11-24 05:31编辑:服务器网络浏览(147)

    简介

    本文介绍了webpack4.0打包优化策略整理小结,分享给大家,具体如下:

    最近在做的项目因为相对较大(打包有100多个chunk),在build构建的时候速度一直上不去,甚是烦恼。由于用的是vue-cli的webpack2模板,一开始并没有想着要对其进行优化,一直觉得是webpack本身慢 硬件慢(在开发机上开发,内存和CPU都不是很强力)的原因。后来慢到实在受不了了,转移到本地(i7 16G)开发的时候,发现生产构建居然需要90s,实在太长了。所以开始着手Webpack2构建优化。

    在前文 Webpack 打包优化之体积篇中,对如何减小 Webpack 打包体积,做了些探讨;当然,那些法子对于打包速度的提升,也是大有裨益。然而,打包速度之于开发体验及时构建,相当重要;所以有必要对其做更为深入的研究,以便完善工作流,这就是本文存在的缘由。

    webpack优化方案

    读了《深入浅出webpack》总结一下常用的webpack的构建优化策略,可通过以下手段来提升项目构建时的速度

    webapck4 新特性介绍-参考资料

    • 优化webpack构建速度,总的来说有几个思路:

    Webpack Package optimization

    1. 优化开发体验

    更精准的loader规则

    1.优化loader配置

    1. 优化本身项目结构,模块的引入、拆分、共用
    2. 优化结构路径、让webpack接管的文件能够快速定位
    3. 优化uglify编译速度
    4. 优化webpack本身编译速度
      有些是在开发的时候代码层面上的,有些则是需要在webpack配置层面上的。对于开发层面上来说,按需引入是很重要的一点。通常为了方便我们可以直接引入一个echarts,但是实际上并不需要echarts的所有功能。而按需引入则能最大程度上让

    减小文件搜索范围

    在使用实际项目开发中,为了提升开发效率,很明显你会使用很多成熟第三方库;即便自己写的代码,模块间相互引用,为了方便也会使用相对路劲,或者别名(alias);这中间如果能使得 Webpack 更快寻找到目标,将对打包速度产生很是积极的影响。于此,我们需要做的即:减小文件搜索范围,从而提升速度;实现这一点,可以有如下两法:

        1-1. 加快构建速度

         ① 缩小文件搜索范围

    • 由于 Loader 对文件的转换操作很耗时,需要让尽可能少的文件被 Loader 处理,用include和exclude去缩小;
    • resolve.modules用于配置 Webpack 去哪些目录下寻找第三方模块:[path.resolve(__dirname, 'node_modules')](根目录下);

    • resolve.mainFields用于配置第三方模块使用哪个入口文件:['mian'];

    • resolve.alias配置项通过别名来把原导入路径映射成一个新的导入路径:[path.resolve(__dirname, './node_modules/react/dist/react.min.js')];
    • resolve.extensions用于配置在尝试过程中用到的后缀列表:['js', 'json'];
    • module.noParse配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析处理:[/react.min.js$/];

         ② HappyPack

    • HappyPack 的核心原理就是把loader转换任务分解到多个进程去并行处理,从而减少了总的构建时间;

         ③ ParallelUglifyPlugin 

    • 压缩js代码需要先把代码解析成用 Object 抽象表示的 AST 语法树,再去应用各种规则分析和处理 AST;
    • ParallelUglifyPlugin会开启多个子进程,把对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过 UglifyJS 去压缩代码,但是变成了并行执行;       

    将loader规则写清楚

    1.1 缩小文件匹配范围

    • 总得来说作用最大的有这几个:

    配置 resolve.modules

    Webpack的resolve.modules配置模块库(即 node_modules)所在的位置,在 js 里出现 import 'vue' 这样不是相对、也不是绝对路径的写法时,会去 node_modules 目录下找。但是默认的配置,会采用向上递归搜索的方式去寻找,但通常项目目录里只有一个 node_modules,且是在项目根目录,为了减少搜索范围,可以直接写明 node_modules 的全路径;同样,对于别名(alias)的配置,亦当如此:

    function resolve (dir) {
      return path.join(__dirname, '..', dir)
    }
    
    module.exports = {
      resolve: {
        extensions: ['.js', '.vue', '.json'],
        modules: [
          resolve('src'),
          resolve('node_modules')
        ],
        alias: {
          'vue$': 'vue/dist/vue.common.js',
          'src': resolve('src'),
          'assets': resolve('src/assets'),
          'components': resolve('src/components'),
          // ...
          'store': resolve('src/store')
        }
      },
      ...
    }
    

    需要额外补充一点的是,这是 Webpack2.* 以上的写法。在 1.* 版本中,使用的是 resolve.root,如今已经被弃用为 resolve.modules;同时被弃用的还有resolve.fallbackresolve.modulesDirectories

        1-2. 提升开发效率

         ① 自动刷新(webpack 模块负责监听文件,webpack-dev-server 模块则负责刷新浏览器)

    • 文件监听(--watch或watch: true),原理是定时的去获取这个文件的最后编辑时间,每次都存下最新的最后编辑时间,如果发现当前获取的和最后一次保存的最后编辑时间不一致,就认为该文件发生了变化;
    • 优化文件监听:watchOptions: {ignored: /node_modules/};
    • 自动刷新原理是往要开发的网页中注入代理客户端代码,通过代理客户端向DevServer 发起的 WebSocket 连接去刷新整个页面;
    • devServer.inline是用来控制是否往 Chunk 中注入代理客户端的,默认会注入;在你关闭了 inline 后,DevServer 会自动地提示你通过新网址 http://localhost:8080/webpack-dev-server/ 去访问;

         ② 模块热替换

    • 模块热替换的原理和自动刷新原理类似,都需要往要开发的网页中注入一个代理客户端用于连接 DevServer 和网页, 不同在于模块热替换独特的模块替换机制;

    仅让需要处理的文件,进入loader处理环节,如下

    通过排除node_modules下的文件 从而缩小了loader加载搜索范围 高概率命中文件

    1. 开启webpack的cache
    2. 开启babel-loader的cache
    3. 指定modules以及配置项目相关的alias
    4. 配置loader的include和exclude
    5. 用CommonsChunkPlugin提取公用模块
    6. 使用DllPlugin和DllReferencePlugin预编译
    7. 换用happypack多进程构建
    8. css-loader换成0.14.5版本。
    9. 换用webpack-uglify-parallel并行压缩代码

    设置 test & include & exclude

    Webpack 的装载机(loaders),允许每个子项都可以有以下属性:

    test:必须满足的条件(正则表达式,不要加引号,匹配要处理的文件)
    exclude:不能满足的条件(排除不处理的目录)
    include:导入的文件将由加载程序转换的路径或文件数组(把要处理的目录包括进来)
    loader:一串“!”分隔的装载机(2.0版本以上,"-loader"不可以省略)
    loaders:作为字符串的装载器阵列

    对于include,更精确指定要处理的目录,这可以减少不必要的遍历,从而减少性能损失。同样,对于已经明确知道的,不需要处理的目录,则应该予以排除,从而进一步提升性能。假设你有一个第三方组件的引用,它肯定位于node_modules,通常它将有一个 src 和一个 dist 目录。如果配置 Webpack 来排除 node_modules,那么它将从 dist 已经编译的目录中获取文件。否则会再次编译它们。故而,合理的设置 include & exclude,将会极大地提升 Webpack 打包优化速度,比如像这样:

    module: {
      preLoaders: [
        {
          test: /.js$/,
          loader: 'eslint',
          include: [resolve('src')],
          exclude: /node_modules/
        },
        {
          test: /.svg$/,
          loader: 'svgo?'   JSON.stringify(svgoConfig),
          include: [resolve('src/assets/icons')],
          exclude: /node_modules/
        }
      ],
      loaders: [
        {
          test: /.vue$/,
          loader: 'vue-loader',
          include: [resolve('src')],
          exclude: /node_modules/(?!(autotrack|dom-utils))|vendor.dll.js/
        },
        {
          test: /.(png|jpe?g|gif|svg)(?.*)?$/,
          loader: 'url',
          exclude: /assets/icons/,
          query: {
            limit: 10000,
            name: utils.assetsPath('img/[name].[hash:7].[ext]')
          }
        }
      ]
    }
    

    2. 优化输出质量

     rules: [{ // 正则尽量准确 test: /.js$/, // 使用缓存,缓存后在文件未改变时编译会更快 use: ['babel-loader?cacheDirectory'], // 指定需要处理的目录 include: path.resolve // 理论上只有include就够了,但是某些情况需要排除文件的时候可以用这个,排除不需要处理文件 // exclude: [] }]
    
     module: { rules: [ { test: /.js$/, use: 'babel-loader', exclude: /node_modules/, // 排除不处理的目录 include: path.resolve // 精确指定要处理的目录 } ] }
    

    以下的配置都是基于vue-cli的webpack模板进行的优化。

    增强代码代码压缩工具

    Webpack 默认提供的 UglifyJS 插件,由于采用单线程压缩,速度颇慢 ;推荐采用 webpack-parallel-uglify-plugin 插件,她可以并行运行 UglifyJS 插件,更加充分而合理的使用 CPU 资源,这可以大大减少的构建时间;当然,该插件应用于生产环境而非开发环境,其做法如下,

    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      },
      sourceMap: true
    })
    

    替换如上自带的 UglifyJsPlugin 写法为如下配置即可:

    var ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
    new ParallelUglifyPlugin({
      cacheDir: '.cache/',
      uglifyJS:{
        output: {
          comments: false
        },
        compress: {
          warnings: false
        }
      }
    })
    

    当然也有其他同类型的插件,比如:webpack-uglify-parallel,但根据自己实践效果来看,并没有 webpack-parallel-uglify-plugin 表现的那么卓越,有兴趣的朋友,可以更全面的做下对比,择优选用。需要额外说明的是,webpack-parallel-uglify-plugin 插件的运用,会相对 UglifyJsPlugin 打出的包,看起来略大那么一丢丢(其实可以忽略不计);如果在你使用时也是如此,那么在打包速度跟包体积之间,你应该有自己的抉择。

        2-1. 减少加载时间

         ① 区分环境

    • process.env.NODE_ENV

         ② 压缩代码

         ③ CDN加速

    • 针对 HTML 文件:不开启缓存,把 HTML 放到自己的服务器上,而不是 CDN 服务上,同时关闭自己服务器上的缓存。自己的服务器只提供 HTML 文件和数据接口;
    • 针对静态的 JavaScript、CSS、图片等文件:开启 CDN 和缓存,上传到 CDN 服务上去,同时给每个文件名带上由文件内容算出的 Hash 值;

         ④ tree shaking

    • 让 Tree Shaking 正常工作的前提是交给 Webpack 的js代码必须是采用 ES6 模块化语法的, 因为 ES6 模块化语法是静态的(导入导出语句中的路径必须是静态的字符串,而且不能放入其它代码块中),这让 Webpack 可以简单的分析出哪些export的被import过了

         ⑤ 提取公共代码(CommonsChunkPlugin)

         ⑥ 按需加载

    更精准的查找目录

    1.2 缓存loader的执行结果

    • 开启webpack的cache

    用 Happypack 来加速代码构建

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

    Webpack-Happypack

    Happypack 的处理思路是:将原有的 webpackloader 的执行过程,从单一进程的形式扩展多进程模式,从而加速代码构建;原本的流程保持不变,这样可以在不修改原有配置的基础上,来完成对编译过程的优化,具体配置如下:

    var HappyPack = require('happypack');
    var happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
    
    module: {
      loaders: [
        {
          test: /.js[x]?$/,
          include: [resolve('src')],
          exclude: /node_modules/,
          loader: 'happypack/loader?id=happybabel'
        }
      ]
    },
    plugins: [
      new HappyPack({
        id: 'happybabel',
        loaders: ['babel-loader'],
        threadPool: happyThreadPool,
        cache: true,
        verbose: true
      })
    ]
    

    可以研究看到,通过在 loader 中配置直接指向 happypack 提供的 loader,对于文件实际匹配的处理 loader,则是通过配置在 plugin 属性来传递说明,这里 happypack 提供的 loader 与 plugin 的衔接匹配,则是通过id=happybabel来完成。配置完成后,laoder的工作模式就转变成了如下所示:

    Webpack-Happypack

    Happypack 在编译过程中,除了利用多进程的模式加速编译,还同时开启了 cache 计算,能充分利用缓存读取构建文件,对构建的速度提升也是非常明显的;更多关于 happyoack 个中原理,可参见 @淘宝前端团队(FED) 的这篇:happypack 原理解析。如果你使用的 Vue.js 框架来开发,也可参考 vue-webpack-happypack 相关配置。

        2-2. 提升流畅度

         ① Prepack

    • 通过 Babel 把 JavaScript 源码解析成抽象语法树(AST),以方便更细粒度地分析源码;
    • Prepack 实现了一个 JavaScript 解释器,用于执行源码。借助这个解释器 Prepack 才能掌握源码具体是如何执行的,并把执行过程中的结果返回到输出中。

         ② Scope Hoisting

     

    理论上我们项目的第三方依赖均应在自己的工程的node_modules下,所以我们可以设置查找目录,减少node的默认查找

    cacheDirectory是loader的一个特定的选项,默认值是false。指定的目录(use: 'babel-loader?cacheDirectory=cacheLoader')将用来缓存loader的执行结果,减少webpack构建时Babel重新编译过程。如果设置一个空值(use: 'babel-loader?cacheDirectory') 或true(use: 'babel-loader?cacheDirectory=true') 将使用默认的缓存目录(node_modules/.cache/babel-loader),如果在任何根目录下都没有找到 node_modules 目录,将会降级回退到操作系统默认的临时文件目录。

    打开webpack.base.conf.js,在module.exports里加上cache: true:

    设置 babel 的 cacheDirectory 为true

    babel-loader is slow! 所以不仅要使用excludeinclude,尽可能准确的指定要转化内容的范畴,而且要充分利用缓存,进一步提升性能。babel-loader 提供了 cacheDirectory特定选项(默认 false):设置时,给定的目录将用于缓存加载器的结果。

    未来的 Webpack 构建将尝试从缓存中读取,以避免在每次运行时运行潜在昂贵的 Babel 重新编译过程。如果值为空(loader: 'babel-loader?cacheDirectory')或true(loader: babel-loader?cacheDirectory=true),node_modules/.cache/babel-loadernode_modules 在任何根目录中找不到任何文件夹时,加载程序将使用默认缓存目录或回退到默认的OS临时文件目录。实际使用中,效果显著;配置示例如下:

    rules: [
      {
        test: /.js$/,
        loader: 'babel-loader?cacheDirectory=true',
        exclude: /node_modules/,
        include: [resolve('src'), resolve('test')]
      },
      ... ...
    ]
    

    webpack构建流程

    1. 初始化(启动构建,读取并合并配置参数,加载plugin,实例化compiler)
    2. 编译(entry出发,针对每个module引用loader解析,依次找到module依赖,然后递归)
    3. 输出(对编译后的module组合成chunk,转换成文件,输出)
    4. 文件变化,转到step2;

        

    module.exports = { resolve: { // 指定当前目录下的node_modules目录 modules: [path.resolve(__dirname, 'node_modules')] }}
    
    module: { rules: [ { test: /.js$/, use: 'babel-loader?cacheDirectory', // 缓存loader执行结果 发现打包速度已经明显提升了 exclude: /node_modules/, include: path.resolve } ]}
    
    module.exports = {
      cache: true,
      // ... 其他配置
    }
    

    设置 noParse

    如果你确定一个模块中,没有其它新的依赖,就可以配置这项, Webpack 将不再扫描这个文件中的依赖,这对于比较大型类库,将能促进性能表现,具体可以参见以下配置:

    module: {
      noParse: /node_modules/(element-ui.js)/,
      rules: [
        {
          ...
        }
    }
    

    更精准的扩展名

    2.resolve优化配置

    • 开启babel-loader的cache
      开启了cache的babel-loader,在下次编译的时候,遇到不变的部分可以直接拿取cache里的内容,能够较为明显地提高构建速度。在loader选项里只需要对babel-loader开启cacheDirectory=true即可。
      配置cacheDirectory后,babel-loader可以缓存处理过的模块,对于没有修改过的文件不会再重新编译。

      // ... 其他配置
      module: {
        rules: [
      {
        test: /.js$/,
        loader: 'babel-loader?cacheDirectory=true'
      },
      // ... 其他loader
        ]
      }
      
    • 配置modules以及配置项目相关的alias

    拷贝静态文件

    在前文 Webpack 打包优化之体积篇中提到,引入 DllPluginDllReferencePlugin 来提前构建一些第三方库,来优化 Webpack 打包。而在生产环境时,就需要将提前构建好的包,同步到 dist 中;这里拷贝静态文件,你可以使用 copy-webpack-plugin 插件:把指定文件夹下的文件复制到指定的目录;其配置如下:

    var CopyWebpackPlugin = require('copy-webpack-plugin')
    
    plugins: [
      ...
      // copy custom static assets
      new CopyWebpackPlugin([
        {
          from: path.resolve(__dirname, '../static'),
          to: config.build.assetsSubDirectory,
          ignore: ['.*']
        }
      ])
    ]
    

    当然,这种工作,实现的法子很多,比如可以借助 shelljs,可以参见这里的实现 vue-boilerplate-template。

    于深圳.南山 @17-08-10 Last Modify: @17-08-12


    数量更多类型的文件尽量放在前面

    2.1 优化模块查找路径 resolve.modules

    这个部分的配置实际上都是对webpack接管的文件路径的一些配置。通过这些配置,webpack可以不必自己遍历去搜索模块等,而可以通过我们定义的路径,快速定位。尤其是node_modules的位置,这个位置可以通过modules选项配置,节省webpack去查找的时间。

    猜你喜欢(/对你有用)的文章

    Webpack 打包优化之体积篇
    『引』最全前端资源汇集
    所历前端“姿势”更替记(其一)
    Vue ES6 Jade Scss Webpack Gulp
    Vue Webpack 组件化开发实践

    平时写代码,我们都习惯直接写文件名,而不去写扩展名,那么解析则按照下面属性进行解析

    Webpack的resolve.modules配置模块库所在的位置,在 js 里出现 import 'vue' 这样不是相对、也不是绝对路径的写法时,会去 node_modules 目录下找。但是默认的配置,会采用向上递归搜索的方式去寻找,但通常项目目录里只有一个 node_modules,且是在项目根目录,为了减少搜索范围,可以直接写明 node_modules 的全路径;同样,对于别名的配置,亦当如此:

    而alias是别名。通过编写alias,既能让webpack查找文件定位更快,在开发的时候,也能少些很多相对路径的../..,在引入模块的时候很方便。

    module.exports = { extensions: ['.js', '.jsx', '.ts', '.tsx'],}
    
    extensions: [".js", ".json"]
    
    const path = require;function resolve { // 转换为绝对路径 return path.join;}resolve: { modules: [ // 优化模块查找路径 path.resolve, path.resolve // 指定node_modules所在位置 当你import 第三方模块时 直接从这个路径下搜索寻找 ]}
    

    同样是打开webpack.base.conf.js,在module.exports的resolve属性里配置modules和alias。其中vue-cli会自动配置一些默认的alias。

    使用动态链接库预编译大模块

    配置好src目录所在位置后,由于util目录是在src里面 所以可以用下面方式引入util中的工具函数

    resolve: {
      //... 其他配置
      modules: [path.resolve(__dirname, '../../node_modules')], // node_modules文件夹所在的位置取决于跟webpack.base.conf.js相对的路径
      alias: {
        //... 其他配置
        api: path.resolve(__dirname, '../../server/api') // api文件所在的位置取决于跟webpack.base.conf.js相对的路径,在项目中会自动转换跟项目文件的相对路径
        //... 其他配置
      }
    }
    

    使用动态链接库,提前编译大模块

    // main.jsimport dep1 from 'util/dep1';import add from 'util/add';
    

    0打包优化计策整理小结_javascript技术_脚本之家,打包优化之速度篇。如果配置了如上的alias,那么我们在项目里,要引用比如api.js这个模块,可以直接这样做了:

    新建一个文件webpack_dll.config.js,内容如下

    2.2 resolve.alias 配置路径别名

    import * as api from 'api' // 'api'是个alias,webpack会直接去找`server/api`
    
    const path = require;const webpack = require;// 复用的大模块放在这里,这样每次都不需要重新编译了const vendors = [ 'react', 'react-dom', 'lodash'];module.exports = { mode: 'development', output: { path: path.resolve, filename: '[name].js', library: '[name]', }, entry: { vendors, }, plugins: [ new webpack.DllPlugin({ path: path.resolve(__dirname, './dist/manifest.json'), name: '[name]', }), ],};
    

    创建 import 或 require 的路径别名,来确保模块引入变得更简单。配置项通过别名来把原导入路径映射成一个新的导入路径 此优化方法会影响使用Tree-Shaking去除无效代码

    而不用手动去根据项目文件和api所在路径的相对位置去书写import的路径了。

    执行webpack --config webpack_dll.config.js进行首次编译

    例如,一些位于 src/ 文件夹下的常用模块:

    • 配置loader的include和exclude

    然后在你的webpack配置文件中引入manifest.json

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

    loader的include和exclude也就是需要loader接管编译的文件和不需要loader接管编译的文件。

     plugins: [ new webpack.DllReferencePlugin({ manifest: require('./dist/manifest.json') }) ],
    

    现在,替换「在导入时使用相对路径」这种方式,就像这样:

    这里我们举babel-loader为例。通常情况下,我们不需要loader去编译node_modules下的js文件,而我们只需要编译我们项目目录下的js就行了。这样可以通过配置这两个选项,能够最小范围的限制babel-loader需要编译的内容,能够有效提升构建速度。

    多进程处理文件

    import Utility from '../../utilities/utility';
    
    import Utility from 'Utilities/utility';
    
    resolve: { alias: { // 别名配置 通过别名配置 可以让我们引用变的简单 'vue$': 'vue/dist/vue.common.js', // $表示精确匹配 src: resolve // 当你在任何需要导入src下面的文件时可以 import moduleA from 'src/moduleA' src会被替换为resolve 返回的绝对路径 而不需要相对路径形式导入 }}
    

    同样打开webpack.base.conf.js,在rules的babel-loader那块加上include和exclude。

    使用HappyPack同时处理多个loader编译任务

    也可以在给定对象的键后的末尾添加 $,以表示精准匹配:

    // ... 其他配置
    module: {
      rules: [
        {
          test: /.js$/,
          loader: ['babel-loader?cacheDirectory=true'],
          include: [resolve('src')], // src是项目开发的目录
          exclude: [path.resolve('../../node_modules')] // 不需要编译node_modules下的js 
        },
        // ... 其他loader
      ]
    }
    

    为了发挥多核CPU电脑的功能,利用HappyPack将任务分发给多个子进程并发执行

    alias: { util$: resolve}
    
    import Test1 from 'util'; // 精确匹配,所以 src/util/add.js 被解析和导入import Test2 from 'util/dep1.js'; // 精确匹配,触发普通解析 util/dep1.js
    
    • 使用CommonsChunkPlugin提取公用模块
    const path = require;const HappyPack = require;// 共享5个进程池const happyThreadPool = HappyPack.ThreadPool;module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve, }, module: { // noParse: [/react.production.min.js$/], rules: [{ test: /.js$/, // 和下面插件id一直,happypack才可以找到 use: ['happypack/loader?id=babel'], include: path.resolve }] }, plugins: [ // 插件可以实例化多个 new HappyPack({ // 与上面对应 id: 'babel', // 实际要使用的loader loaders: ['babel-loader?cacheDirectory'], // 默认开启进程数 threads: 3, // 是否允许happyPack打印日志 verbose: true, // 共享进程数,如果你使用超过一个happyplugin,官方建议共享进程池 threadPool: happyThreadPool }) ],};
    

    2.3resolve.extensions

    我们经常会有这种场景:在a.vue组件里引入了a.js或者比如c.vue,在b.vue组件里也引入了a.js或者c.vue。这样,打包了之后将会把引入的模块重复打包。而CommonsChuncksPlugin就是把这样重复打包的模块给抽取出来单独打包的插件。这个能够显著降低最后打包的体积,也能提升一些打包速度。

    多进程压缩文件

    当引入模块时不带文件后缀 webpack会根据此配置自动解析确定的文件后缀

    在webpack.base.conf.js里的plugins可以加上这段:

    使用ParallelUglifyPlugin多进程同时压缩文件

    后缀列表尽可能小 频率最高的往前放 导出语句尽可能带上后缀

    plugins: [
      new webpack.optimize.CommonsChunkPlugin({
        async: 'shared-module',
        minChunks: (module, count) => (
          count >= 2    // 当一个模块被重复引用2次或以上的时候单独打包起来。 
        )
      }),
      //...
    ]
    

    ParallelUglifyPlugin是在UglifyJS基础上,增加了多进出处理的能力,加快了压缩速度

    resolve: { extensions: ['.js', '.vue']}
    
    • 使用DllPlugin和DllReferencePlugin预编译
    import ParallelUglifyPlugin from 'webpack-parallel-uglify-plugin'; module.exports = { plugins: [ new ParallelUglifyPlugin({ test, include, exclude, cacheDir, workerCount, sourceMap, uglifyJS: { }, uglifyES: { } }), ],};
    

    3.module.noParse

    这个也是一个大杀器。将一些全局都要用到的依赖抽离出来,预编译一遍,然后引入项目中,作为依赖项。而webpack在正式编译的时候就可以放过他们了。能够很明显地提升webpack的构建速度。类似于Windows的dll文件的设计理念。dll资源能够有效的解决资源循环依赖的问题。能够大大减少项目里重复依赖的问题。

    减少监听文件

    用了noParse的模块将不会被loaders解析,所以当我们使用的库如果太大,并且其中不包含import require、define的调用,我们就可以使用这项配置来提升性能, 让 Webpack 忽略对部分没采用模块化的文件的递归解析处理。

    在webpack.base.conf.js所在的文件夹里建立一个webpack.dll.conf.js,我们将一些常用的依赖打包成dll。

    减少监听文件

    // 忽略对jquery lodash的进行递归解析module: { // noParse: /jquery|lodash/ // 从 webpack 3.0.0 开始 noParse: function { return /jquery|lodash/.test }}
    

    首先配置一下DllPlugin的资源列表。

    当我们使用webpack的watch功能进行文件监听时,更好的方式是控制监听目录,如下,排除node_modules减少对该目录监听,减少编译所需要循环的文件,提高检查速度

    4.HappyPack

    const path = require('path');
    const webpack = require('webpack');
    module.exports = {
      entry: {
        vendor: ['vue/dist/vue.esm.js','vue-router','axios','vuex'] // 需要打包起来的依赖
      },
      output: {
        path: path.join(__dirname, '../../public/js'), // 输出的路径
        filename: '[name].dll.js', // 输出的文件,将会根据entry命名为vendor.dll.js
        library: '[name]_library' // 暴露出的全局变量名
      },
      plugins: [
        new webpack.DllPlugin({
          path: path.join(__dirname, '../../public/js/', '[name]-mainfest.json'), // 描述依赖对应关系的json文件
          name: '[name]_library', 
          context: __dirname // 执行的上下文环境,对之后DllReferencePlugin有用
        }),
        new webpack.optimize.UglifyJsPlugin({ // uglifjs压缩
          compress: {
            warnings: false
          }
        })
      ]
    }
    
    module.export = { watchOptions: { ignored: /node_modules/ }}
    

    HappyPack是让webpack对loader的执行过程,从单一进程形式扩展为多进程模式,也就是将任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。从而加速代码构建 与 DLL动态链接库结合来使用更佳。

    为了方便之后构建,可以在package.json里加上这么一句scripts:

    其他没那么重要的优化

    npm i happypack@next -D
    
    scripts: {
      //... 其他scripts
      "build:dll": "webpack --config ./webpack/build/webpack.dll.conf.js" // 填写你项目中webpack.dll.conf.js的路径
    }
    

    更精准的mainFields

    webpack.config.js

    然后执行一下npm run build:dll,就可以在输出的目录里输出一个vendor.dll.js和vendor-mainfest.json两个文件。

    默认的这个值查找方式见官网点击此处

    const HappyPack = require;const os = require; // node 提供的系统操作模块 // 根据我的系统的内核数量 指定线程池个数 也可以其他数量const happyThreadPool = HappyPack.ThreadPool.lenght})module: { rules: [ { test: /.js$/, use: 'happypack/loader?id=babel', exclude: /node_modules/, include: path.resolve } ]},plugins: [ new HappyPack({ // 基础参数设置 id: 'babel', // 上面loader?后面指定的id loaders: ['babel-loader?cacheDirectory'], // 实际匹配处理的loader threadPool: happyThreadPool, // cache: true // 已被弃用 verbose: true });]
    

    之后打开webpack.base.conf.js。在plugins一项里加上DllReferencePlugin。这个plugin是用于引入上一层里生成的json的。

    看了下react和lodash,只有一个main,目前来看使用es6看来还不普遍,所以这个值目前可能不太重要

    happypack提供的loader,是对文件实际匹配的处理loader。这里happypack提供的loader与plugin的衔接匹配,则是通过id=happypack来完成。

    module.exports = {
      //... 其他配置
    
      plugins: [
        // ... 其他插件
        new webpack.DllReferencePlugin({
          context: __dirname,
          manifest: require('../../public/js/vendor-mainfest.json') // 指向这个json
        })
      ]
    }
    
    module.exports = { resolve: { mainFields: ['main'] }}
    
    npm run dev
    

    最后,在项目输出的index.html里,最先引入这个js:

    为什么这个不重要,我发现react直接导出的index.js则是根据环境判断使用哪份代码,目测来看并不需要进行循环依赖的处理

    5.DLL动态链接库

    <script type="text/javascript" src="public/js/vendor.dll.js"></script>
    

    通过依赖,则可以直接使用打包后代码,而不需webpack去循环依赖

    在一个动态链接库中可以包含其他模块调用的函数和数据,动态链接库只需被编译一次,在之后的构建过程中被动态链接库包含的模块将不会被重新编译,而是直接使用动态链接库中的代码。

    这样,webpack将不会再解析dll里的资源了。构建速度将会有质的提高。

     resolve: { mainFields: ["main"], alias: { 'react': path.resolve(__dirname, './node_modules/react/cjs/react.production.min.js') } }
    

    将web应用依赖的基础模块抽离出来,打包到单独的动态链接库中。一个链接库可以包含多个模块。 当需要导入的模块存在于动态链接库,模块不会再次打包,而是去动态链接库中去获取。 页面依赖的所有动态链接库都需要被加载。

    • 换用happypack多进程构建

    不使用inline模式的devServer

    5.1 定义DLL配置

    webpack的构建毕竟还是单进程的。采用happypack可以改为多进程构建。而对于小文件而言,happypack效果并不明显。而对于babel-loader编译的庞大的js文件群来说,则是一大利器。

    默认情况下,应用程序启用内联模式。这意味着一段处理实时重载的脚本被插入到你的包中,并且构建消息将会出现在浏览器控制台。

    依赖的两个内置插件:DllPlugin 和 DllReferencePlugin

    首先安装:npm install happypack --save-dev或者yarn add happypack

    当使用inline模式时,devServer会向每个Chunk中注入一段重载的脚本代码,但是其实一个页面只需要一次,所以当Chunk过多时,可以将inline设置为false

    5.1.1 创建一个DLL配置文件webpack_dll.config.js

    然后修改webpack.base.conf.js的配置如下:

    module.export = { devServer: { inline: false }}
    
    module.exports = { entry: { react: ['react', 'react-dom'] }, output: { filename: '[name].dll.js', // 动态链接库输出的文件名称 path: path.join, // 动态链接库输出路径 libraryTarget: 'var', // 链接库输出方式 默认'var'形式赋给变量 b library: '_dll_[name]_[hash]' // 全局变量名称 导出库将被以var的形式赋给这个全局变量 通过这个变量获取到里面模块 }, plugins: [ new webpack.DllPlugin({ // path 指定manifest文件的输出路径 path: path.join(__dirname, 'dist', '[name].manifest.json'), name: '_dll_[name]_[hash]', // 和library 一致,输出的manifest.json中的name值 }) ]}
    
    const os = require('os');
    const HappyPack  = require('happypack');
    const happThreadPool = HappyPack.ThreadPool({size: os.cpus().length}); // 采用多进程,进程数由CPU核数决定
    //...
    module.exports = {
      plugins: [
        // ...
        new HappyPack({
          id: 'js',
          cache: true,
          loaders: ['babel-loader?cacheDirectory=true'],
          threadPool: happThreadPool
        })
      ],
      module: {
        rules: [
          {
            test: /.vue$/,
            loader: 'vue-loader',
            options: {
              loaders: {
                js: 'happypack/loader?id=js' // 将loader换成happypack
              }
            }
          },
          {
            test: /.js$/,
            loader: ['happypack/loader?id=js'], // 将loader换成happypack
            include: [resolve('src')], // src是项目开发的目录
            exclude: [path.resolve('../../node_modules')] // 不需要编译node_modules下的js 
          },
          //...
        ]
      }
    }
    

    补充

    5.1.2 output.libraryTarget 规定了以哪一种导出你的库 默认以全局变量形式 浏览器支持的形式

    提速还是比较明显的。

    补充1-cacheDirectory原理

    "var" - 以直接变量输出 var Library = xxx "this" - 通过设置this的属性输出 this["Library"] = xxx "commonjs" - 通过设置exports的属性输出 exports["Library"] = xxx "commonjs2" - 通过设置module.exports的属性输出 module.exports = xxx "amd" - 以amd方式输出 "umd" - 结合commonjs2/amd/root

    • css-loader换成0.14.5版本

    当有设置cacheDirectory时,指定的目录将用来缓存loader的执行结果。之后的 webpack 构建,将会尝试读取缓存,来避免在每次执行时,可能产生的、高性能消耗的Babel重新编译过程。如果设置了一个空值 (loader: 'babel-loader?cacheDirectory') 或者 true (loader: babel-loader?cacheDirectory=true),loader 将使用默认的缓存目录 node_modules/.cache/babel-loader,如果在任何根目录下都没有找到 node_modules 目录,将会降级回退到操作系统默认的临时文件目录。

    5.1.3 打包生成动态链接库

    可以查看这个issue,说是该版本之上的版本会拖慢webpack的构建速度。我自己实验了之后确实能快几秒钟。

    补充2-node的默认查找方式

    webpack --config webpack_dll.config.js --mode production
    
    • 换用webpack-uglify-parallel并行压缩代码

    查找当前目录下的node_modules目录,看是否有匹配项,如有,命中文件 寻找父目录的下的node_modules,如有,命中文件 按照这个规则一直往父目录搜索直到到根目录下的node_modules

    在dist目录下 多出react.dll.js 和 react.manifest.json

    webpack自带的uglifyjs插件效果确实不错。只不过由于受限于单线程,所以压缩速度不够高。换成webpack-uglify-parallel这个插件之后能够有效减少压缩的时间。

    补充3-动态链接库思想

    react.dll.js 动态链接库 里面包含了 react和react-dom的内容 react.manifest.json 描述链接库中的信息

    首先安装:npm install webpack-uglify-parallel --save-dev 或者 yarn add webpack-uglify-parallel

    大量项目中可以复用的模块只需要被编译一次,在之后的构建过程中被动态链接库包含的模块将不会重新编译,而是直接使用动态链接库中的代码。(注:如果升级依赖的模块版本,需要重新编译动态链接库)

    5.2 在主配置文件中使用动态链接库文件

    找到webpack.prod.conf.js(由于开发模式不需要进行uglify压缩),将原本的:

    补充4-HappyPack原理

    // webpack.config.jsconst webpack = require;plugins: [ // 当我们需要使用动态链接库时 首先会找到manifest文件 得到name值记录的全局变量名称 然后找到动态链接库文件 进行加载 new webpack.DllReferencePlugin({ manifest: require('./dist/react.manifest.json') })]
    
    plugins: [
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false
        },
        sourceMap: true
      })
      // ... 其他配置
    ]
    

    webpack构建中,需要大量的loader转换操作,很耗时,由于nodejs是单线程的,如果想更好利用cpu的多核能力,可以开启多个进程,同时对文件进行处理;可以看到在配置文件中,我们每次将文件交给happypack-loader去处理,然后由happypack去调度来执行文件的处理(happypack采用哪个loaders进行处理,是通过id知道的)

    5.3 将动态链接库文件加载到页面中

    替换为:

    补充5-文件监听原理

    需要借助两个webpack插件

    const UglifyJsparallelPlugin = require('webpack-uglify-parallel');
    const os = require('os');
    // ... 其他配置
    plugins: [
        new UglifyJsparallelPlugin({
            workers: os.cpus().length,
            mangle: true,
            compressor: {
                warnings: false,
                drop_console: true,
                drop_debugger: true
            }
        })        
    ]
    

    webpack会从入口触发,将所有的依赖项放到一个list里边,然后每次修改文件内容,回去遍历整个list里边的文件,看是否有编辑时间的变化,如果有的话则进行编译

    html-webpack-plugin 产出html文件

    • 效果综述

    补充6-自动刷新原理

    html-webpack-include-assets-plugin 将js css资源添加到html中 扩展html插件的功能

    经过以上优化,原本90 s的构建时间,优化到30 秒效果还是很明显的。原本HMR模式初始化需要50 秒,也优化到了20 秒的程度。不过,还是有一个至今我自己还无法解决的问题,那就是HMR模式,rebuild时间会越来越长,直到超过初始化的20 秒。此时只能重新开关一次HMR模式。这一点我还无法找到具体原因是什么。不过,至少生产的构建时间得到了60%的优化,效果还是挺好的。

    向要开发的网页中注入代理客户端代码,通过代理客户端去刷新整个页面 将要开发的网页放进一个iframe,通过刷新iframe去看刷新效果

    npm i html-webpack-plugin html-webpack-include-assets-plugin -D
    

    来源:

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

    配置webpack.config.js

    const webpack = require;const HtmlWebpackPlugin = require('html-webpack-plugin');const HtmlIncludeAssetsPlugin = require('html-webpack-include-assets-plugin');pluings: [ new webpack.DllReferencePlugin({ manifest: require('./dist/react.manifest.json') }), new HtmlWebpackPlugin({ template: path.join(__dirname, 'src/index.html') }), new HtmlIncludeAssetsPlugin({ assets: ['./react.dll.js'], // 添加的资源相对html的路径 append: false // false 在其他资源的之前添加 true 在其他资源之后添加 });]
    

    此时react.dll.js和main.js被自动引入到页面中,并且dll文件在main.js之前加载

    6.ParallelUglifyPlugin

    这个插件可以帮助有很多入口点的项目加快构建速度。把对JS文件的串行压缩变为开启多个子进程并行进行uglify。

    cnpm i webpack-parallel-uglify-plugin -D
    
    // webpck.config.jsconst ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');plugins: [ new ParallelUglifyPlugin({ workerCount: 4, uglifyJS: { output: { beautify: false, // 不需要格式化 comments: false // 保留注释 }, compress: { // 压缩 warnings: false, // 删除无用代码时不输出警告 drop_console: true, // 删除console语句 collapse_vars: true, // 内嵌定义了但是只有用到一次的变量 reduce_vars: true // 提取出出现多次但是没有定义成变量去引用的静态值 } } });]
    
    webpack --mode production
    

    7.Tree Shaking

    剔除JavaScript中用不上的代码。它依赖静态的ES6模块化语法,例如通过impot和export导入导出

    commonJS模块 与 es6模块的区别

    commonJS模块:

    1.动态加载模块 commonJS 是运行时加载 能够轻松实现懒加载,优化用户体验

    2.加载整个模块 commonJS模块中,导出的是整个模块

    3.每个模块皆为对象 commonJS模块被视作一个对象

    4.值拷贝 commonJS的模块输出和函数的值传递相似,都是值得拷贝

    es6模块

    1.静态解析 es6模块时 编译时加载 即在解析阶段就确定输出的模块的依赖关系,所以es6模块的import一般写在被引入文件的开头

    2.模块不是对象 在es6里,每个模块并不会当做一个对象看待

    3.加载的不是整个模块 在es6模块中 一个模块中有好几个export导出

    4.模块的引用 es6模块中,导出的并不是模块的值得拷贝,而是这个模块的引用

    7.1 保留ES6模块化语法

    // .babelrc{ "presets": [ [ "env", { modules: false // 不要编译ES6模块 }, "react", "stage-0" ] ]}
    

    7.2 执行生产编译 默认已开启Tree Shaking

    webpack --mode production
    

    什么是Tree Shaking?

    有个funs.js 里面有两个函数

    // funs.jsexport const sub = () => 'hello webpack!';export const mul = () => 'hello shaking!';
    

    main.js 中依赖funs.js

    // main.jsimport {sub} from './funs.js'sub();
    

    在main.js只使用了里面的 sub函数 默认情况下也会将funs.js里面其他没有的函数也打包进来, 如果开启tree shaking 生产编译时

    webpack --mode production //此时funs.js中没有被用到的代码并没打包进来 而被剔除出去了
    

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

    本文由新葡亰496net发布于服务器网络,转载请注明出处:0打包优化计策整理小结_javascript技术_脚本之家,

    关键词: