深刻理解 Webpack 打包分块(下)

前言

随着前端代码须要处理的业务愈来愈繁重,咱们不得不面临的一个问题是前端的代码体积也变得愈来愈庞大。这形成不管是在调式仍是在上线时都须要花长时间等待编译完成,而且用户也不得不花额外的时间和带宽下载更大致积的脚本文件。javascript

然而仔细想一想这彻底是能够避免的:在开发时难道一行代码的修改也要从新打包整个脚本?用户只是粗略浏览页面也须要将整个站点的脚本所有下载下来?因此趋势必然是按需的、有策略性的将代码拆分和提供给用户。最近流行的微前端某种意义上来讲也是遵循了这样的原则(但也并非彻底基于这样的缘由)css

幸运的是,咱们目前已有的工具已经彻底赋予咱们实现以上需求的能力。例如 Webpack 容许咱们在打包时将脚本分块;利用浏览器缓存咱们可以有的放矢的加载资源。html

在探寻最佳实践的过程当中,最让我疑惑的不是咱们能不能作,而是咱们应该如何作:咱们因该采起什么样的特征拆分脚本?咱们应该使用什么样的缓存策略?使用懒加载和分块是否有殊途同归之妙?拆分以后究竟能带来多大的性能提高?最重要的是,在面多诸多的方案和工具以及不肯定的因素时,咱们应该如何开始?这篇文章就是对以上问题的梳理和回答。文章的内容大致分为两个方面,一方面在思路制定模块分离的策略,另外一方面从技术上对方案进行落地。前端

本文的主要内容翻译自 The 100% correct way to split your chunks with Webpack。 这篇文章按部就班的引导开发者步步为营的对代码进行拆分优化,因此它是做为本文的线索存在。同时在它的基础上,我会对 Webpack 及其余的知识点作纵向扩展,对方案进行落地。java

如下开始正文node


把应用代码进行分离

如今让咱们把目光转向 Alice 一遍又一遍下载的 main.js 文件react

我以前提到过咱们的站点里又两个彻底不一样的部分:一个产品列表页面和一个详情页面。每一个页面独立的代码说起大概是 25KB(共享 150KB 的代码)jquery

咱们的“产品详情”页面目前不会进行更改,由于它很是的完美。因此若是咱们把它划分为独立文件,大部分时候它都可以从缓存中进行加载webpack

你知道咱们还有一个用于渲染 icon 用的 25KB 的几乎不发生修改的 SVG 文件吗?咱们应该对它作些什么git

咱们手动的增长一些 entry 入口,告诉 Webpack 给它们都建立独立的文件:

module.exports = {
  entry: {
    main: path.resolve(__dirname, 'src/index.js'),
    ProductList: path.resolve(__dirname, 'src/ProductList/ProductList.js'),
    ProductPage: path.resolve(__dirname, 'src/ProductPage/ProductPage.js'),
    Icon: path.resolve(__dirname, 'src/Icon/Icon.js'),
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash:8].js',
  },
  plugins: [
    new webpack.HashedModuleIdsPlugin(), // so that file hashes don't change unexpectedly
  ],
  optimization: {
    runtimeChunk: 'single',
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: Infinity,
      minSize: 0,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name(module) {
            // get the name. E.g. node_modules/packageName/not/this/part.js
            // or node_modules/packageName
            const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];

            // npm package names are URL-safe, but some servers don't like @ symbols
            return `npm.${packageName.replace('@', '')}`;
          },
        },
      },
    },
  },
};
复制代码

而且 Webpack 自动为它们之间的共享代码也建立了独立的文件,也就是说ProductListProductPage不会拥有重复的代码

这回 Alice 在大多数周里都会节省下 50KB 的下载量

只有 1.815MB 了

咱们已经为 Alice 节省了 56% 的下载量,而且会持续下去(在咱们的理论场景中)

全部这些都是经过修改 Webapck 配置实现的——咱们尚未修改任何一行应用程序的代码。

我以前提到测试之下是什么样具体的场景并不重要。由于不管你碰见的是什么场景,结论始终是一致的:把你的代码划分为更多更有意义的小文件,用户须要下载的代码也就越少


很快咱们就将谈到“代码分离”——另外一种分割文件的方式——可是首先我想首先解决你如今正在考虑的问题

网络请求变多的时候是否是会变得更慢?

答案很是明确是否认的

在 HTTP/1.1 的状况下确实会如此,可是在 HTTP/2 中不会

尽管如此,这篇来自 2016 年的文章和来自于Khan Academy 2015 年的文章都得出结论说即便有 HTTP/2 下载太多文件的话仍然会致使变慢。可是在这两篇文章里“太多”意味着上百个文件。因此只要记住若是你有上百个文件,你或许达到了并行的上限

若是你在好奇如何在 Windows 10 的 IE11 上支持 HTTP/2。我对那些还在使用古董机器的人作了调查,他们出奇一致的让我放心他们根本不关心网站的加载速度

每个 webpack 打包后的文件里会不会有多余的模板代码?

有的

但什么是“模板代码”?

想象一下若是整个项目只有文件app.js,那么最终的输出的打包文件也只是app.js的文件内容而已。

可是若是app.js文件内容是空的话(一行代码都没有),那么最终的打包文件也是空的吗?

不是,Webpack 为了实现编译以后的模块化,它会将你的代码进行一次封装,这些用于封装的代码会占用一部分体积,是每一个模块都必须存在的,因此成为模板代码

若是我有多个小文件的话是否是压缩的效果就减弱了

是的

事实确实是:

  • 多文件 = 多 Webpack 模板代码
  • 多文件 = 压缩减少

让咱们把其中的损耗的都明确下来

我刚刚作了一个测试,一个 190 KB 的站点文件被划分为了19个文件,发送给浏览器的字节数大概多了 2%

因此……首次访问的文件说起增长了 2% 可是直到世界末日其余的每次访问文件体积都减少了 60%

因此损耗的正确数字是:一点都不。

当我在测试 1 个文件对比 19 个文件状况时,我想我应该赋予测试一些不一样的网络环境,包括 HTTP/1.1

下面这张表格给予了“文件越多越好”的有力支持

在 3G 和 4G 的状况下当有19个文件时加载时间减小了 30%

但真的是这样吗?

这份数据看上去“噪点”不少,举个例子,在 4G 场景下第二次运行时,网站加载花费了 646ms,可是以后的第二轮运行则花费了 1116ms——时间增长了73% 。因此宣称 HTTP/2 快了 30% 有一些心虚

我建立这张表格是为了试图量化 HTTP/2 究竟能带来多大的差别,可是我惟一能说的是“并无太大的区别”

真正使人惊喜的是最后两行,旧版本的 Windows 和 HTTP/1.1 我本觉得会慢很是多。我猜我须要更慢的网络环境用于进行验证


故事时间!我从微软网站下载了一个 Windows 7 的虚拟机来测试这些东西

我想把默认的 IE8 升级至 IE9

因此我前往微软下载 IE9 的页面而后发现:

最后提一句 HTTP/2,你知道它已经集成进 Node 中了吗?若是你想尝试,我用100行写了一段 HTTP/2 服务,可以为你的测试带来缓存上的帮助


以上就是我想说的关于打包分离的一切。我想这个实践惟一的坏处是须要说服人们加载如此多的小文件是没有问题的

代码分离(没必要加载你不须要的代码)

这个特殊的实践只对某些站点有效

我乐意重申一下我发明的 20/20 理论:若是站点的某些部分只有 20% 用户会访问,而且这部分的脚本量大于你整个站点的 20% 的话,你就应该考虑按需加载代码了

你能够对数值进行调整来适配更复杂的场景。重点是保持平衡,须要决策将对站点无心义的代码分离出来

如何决策

假设你有拥有一个购物网站,你在纠结是否应该把“结帐”功能的代码分离出来,由于只有 30% 的用户会走到那一步

首先是要让卖的更好

其次计算出“结帐”功能的独立代码有多少。由于在作“代码分离”以前你经常作“打包文件分离”,你或许已经知道了这部分代码量有多少

(它可能比你想象的还要小,因此计算以后你可能得到惊喜。若是你有一个 React 站点,你的 store,reducer,routing,actions 可能会被整个网站共享,独立的部分可能大部分是组件和帮助类库)

假设你注意到结算页面独立代码一共只有 7KB,其余部分的代码 300KB。看到这种状况我会建议不把这些代码分开,有如下几个缘由

  • 它并不会让加载变得更慢。记得你以前并行的加载这些文件,你能够试着记录加载 300KB 和 307KB 的文件是否有变化
  • 若是你延迟加载这部分代码,用户在点击“付款”以后仍然须要等待文件的加载——你并不但愿在这关键时刻给用户带来任何的阻力
  • 代码分离会致使程序代码的更改,这须要将以前同步逻辑的地方改成异步逻辑。这并不复杂,可是对于改善用户体验这件事的性价比来讲仍是过于复杂了

这些就是我说的“这项使人振奋的技术或许不适合你”

让咱们看看两个代码分离的例子

回滚方案(Polyfills)

咱们从这个例子开始是由于它适用于大多数站点,而且是一个很是好的入门

我给个人站点使用了一堆酷炫的功,因此我使用了一个文件导入了我须要的全部回滚方案。它只须要八行代码:

require('whatwg-fetch');
require('intl');
require('url-polyfill');
require('core-js/web/dom-collections');
require('core-js/es6/map');
require('core-js/es6/string');
require('core-js/es6/array');
require('core-js/es6/object');
复制代码

我在个人入口文件index.js顶部引入了这个文件

import './polyfills';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App/App';
import './index.css';

const render = () => {
  ReactDOM.render(<App />, document.getElementById('root')); } render(); // yes I am pointless, for now 复制代码

在 Webpack 配置关于打包分离的小节配置中,个人回滚代码会自动被分为四个不一样的文件由于有四个 npm 包。它们一共大小 25KB 左右,而且 90% 的浏览器都不须要它们,因此它们值得动态的进行加载。

在 Webpack 4 以及 import() 语法(不要和import语法混淆了)的支持下,有条件的加载回滚代码变得很是简单了

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App/App';
import './index.css';

const render = () => {
  ReactDOM.render(<App />, document.getElementById('root')); } if ( 'fetch' in window && 'Intl' in window && 'URL' in window && 'Map' in window && 'forEach' in NodeList.prototype && 'startsWith' in String.prototype && 'endsWith' in String.prototype && 'includes' in String.prototype && 'includes' in Array.prototype && 'assign' in Object && 'entries' in Object && 'keys' in Object ) { render(); } else { import('./polyfills').then(render); } 复制代码

如今是否是更有意义了?若是浏览器支持全部的新特性,那么渲染页面。不然加载回滚代码渲染页面。当代码在运行在浏览器中时,Webpack 的运行时会负责这四个包的加载,而且当它们被下载而且解析完毕时,render()函数才会被调用,而且其它工做继续运行

(顺便说一声,若是须要使用import()的话,你须要 Babel 的 dynamic-import 插件 。而且如 Webpack 文档解释的,import()使用 Promises,因此你须要把这部分的回滚代码独立出来)

很是简单不是吗?

有一些更棘手的场景

基于路由的动态加载(针对 React)

回到 Alice 的例子,假设网站如今多了一个“管理”页面,产品的卖家能够登录而且管理他们售卖的产品

这个页面有不少有用的功能,不少的图表,须要安装一个来自 npm 的表单类库。由于我已经实现了打包代码分离,目测至少已经节省了100KB 的大小文件

如今我设置了一份当用户访问呢/admin时渲染<AdminPage>的路由。当 Webpack 把一切都打包完毕以后,它会去查找import AdminPage from './AdminPage.js',而且说“嘿,我须要把它包含到初始化的加载文件中”

可是咱们不想这么作,咱们但愿在动态加载中加载管理页面,好比import('./AdminPage.js'),这样 Webpack 就知道须要动态加载它。

很是酷,不须要任何的配置

与直接引用AdminPage不一样,当用户访问/admin时我使用另一个组件用于实现以下功能:

核心思想很是简单,当组件加载时(也就意味着用户访问/admin时),咱们动态的加载./AdminPage.js而后在组件 state 中保存对它的引用

在渲染函数中,在等待<AdminPage>加载的过程当中咱们简单的渲染出<div>Loading...</div>,一旦加载成功则渲染出<AdminPage>

为了好玩我想本身实现它,可是在真实的世界里你只须要像React 关于代码分离的文档描述的那样使用 react-loadable便可


以上就是全部内容了。以上我说的每个观点,还能说的更精简吗?

  • 若是人们会不止一次的访问你的站点,把你的代码划分为不一样的小文件
  • 若是你的站点有很大一部分用户不会访问到,动态的加载它们

谢谢阅读,祝你有愉快的一天

完蛋了我忘记提 CSS 了

关于开发体验

以上咱们都是在针对 production 对代码进行分割。但事实上咱们在开发过程当中也会面临一样的问题:当代码量增多时,打包的时间也在不断增加。可是例如 node_modules 里的代码千年不变,彻底不须要被从新编译。这部分咱们也能够经过代码分离的思想对代码进行分离。好比 DLL 技术

一般咱们说的 DLL 指的是 Windows 系统的下的动态连接库文件,它的本意是将公共函数库提取出来给你们公用以减小程序体积。咱们的 DLL 也是借助了这种思想,将公共代码分离出来。

使用 DLL 简单来讲分为两步:

输出 DLL 文件

咱们将咱们须要分离的文件到打包为 DLL 文件,以分离 node_modules 类库为例,关键配置以下。注意这段配置仅仅是用于分离 dll 文件,并不是打包应用脚本

module.exports = {
   entry: {
      library: [
         'react',
         'redux',
         'jquery',
         'd3',
         'highcharts',
         'bootstrap',
         'angular'
      ]
   },
   output: {
      filename: '[name].dll.js',
      path: path.resolve(__dirname, './build/library'),
      library: '[name]'
   },
   plugins: [
    new webpack.DllPlugin({
        name: '[name]',
        path: './build/library/[name].json'
    })
  ]
};
复制代码

关键在于使用 DLLPlugin 输出的 json 文件,用于告诉 webpack 从哪找到预编译的类库代码

引入 DLL 文件

在正式打包应用脚本的 Webpack 配置中引入 DLL 便可:

plugins: [
  new webpack.DllReferencePlugin({
    context: __dirname,
    manifest: require('./build/library/library.json')
  })
]
复制代码

不过美中不足的是,你仍然须要在你最终的页面里引入 dll 文件

若是你的以为手动配置 dll 仍然以为繁琐,那么能够尝试使用 AutoDllPlugin

本文同时也发布在个人知乎专栏,欢迎你们关注

参考资料

Bundle VS Chunk

Hash

SplitChunksPlugin

DLL

相关文章
相关标签/搜索