首次公布于:https://moltemort.top/post/yi...

技术积攒,从翻译优质文章开始。本文翻译自:The 100% correct way to split your chunks with Webpack

为本人网站的用户找出推送文件的最好形式是一件很麻烦的事。 太多不同的场景,不同的技术,不同的术语了。

在这篇 Blog 中,我心愿通知你你想晓得的所有,你能够:

  1. 理解哪种文件拆分策略最适宜你的网站和用户
  2. 晓得怎么去做


拆包

依据 Webpack 词汇表,有两种不同类型的文件拆分形式。 这两个概念听起来能够调换,但显然并不是这样:

Bundle splitting:创立更多,更小的文件(但不管怎样,每个网络申请中都要加载它们),来优化缓存。
Code splitting:动静加载代码,用户能够仅下载他们正在查看的网站局部所需的代码。

第二个概念听起来更加吸引人啊,对吧?其实,许多对于此的文章都仿佛认为这是加载 JS 文件的惟一有价值的 case。

然而我来通知你,对于少数站点来说,第一个才是更有价值的形式,而且应该是你对网站所做的首要的事件。

咱们接着深刻往下看。

Bundle splitting

Bundle splitting 背地的想法非常简单。 如果你有一个微小的文件,当你更改了一行代码,用户必须再次下载整个文件。 然而,如果咱们将其拆分为两个文件,则用户只需下载已更改的文件,浏览器会从缓存中读取另一个文件。

值得注意的是,因为 Bundle splitting 是齐全针对缓存的,所以对于首次拜访的用户而言(拆与不拆)没什么区别。

(我认为太多的性能探讨都与首次拜访网站无关。兴许这部分是因为「第一印象很重要」,另一块是因为它很容易量化。)

当波及到频繁拜访的用户时,量化性能优化带来的影响可能很麻烦,但咱们必须量化!

上面是我在前一段中提到的状况:

  • Alice 每周拜访咱们的网站一次,继续10周
  • 咱们每周一次发版
  • 咱们每周都会更新「产品列表」页
  • 咱们还有一个「产品详情」页,当初咱们先不论它
  • 在第5周,咱们向网站增加一个新的 npm 包
  • 第8周,咱们更新了其中一个既有的 npm 包

一些人(比方我)心愿这种状况尽可能符合实际。 这种做法并不好。理论状况并不重要,咱们之后会找出起因。 (先留个铺垫!)

比拟基准值

假如咱们的 JavaScript 包总大小为 400 KB,目前以单个文件main.js的模式加载。

咱们的 Webpack config 看起来像这样(省略了无关的配置内容):

const path = require('path');module.exports = {  entry: path.resolve(__dirname, 'src/index.js'),  output: {    path: path.resolve(__dirname, 'dist'),    filename: '[name].[contenthash].js',  },};

当只有一个入口时,Webpack 会将打包后果 bundle 命名为「main.js」

(对于那些刚开始学习缓存更新策略的人:每当我提到main.js时,实际上是在说main.xMePWxHo.js之类的货色,这一坨疯狂的字母串是文件内容的哈希。这意味着当程序中代码更改时会产生不同的文件名 ,从而迫使浏览器下载新文件。)

每周咱们会对网站进行一些新更改,包中内容哈希都会更改。 所以,在每周 Alice 拜访咱们的网站时,都必须要下载一个新的400 KB文件。

如果咱们要做一个花里胡哨的表,它看起来就像这样。


世界上最没用的 Total 统计

这10周积起来,就是 4.12 MB

咱们能够做得更好。

拆分出 vendor 包

让咱们把包拆成main.jsvendor.js

很简略,相似于:

const path = require('path');module.exports = {  entry: path.resolve(__dirname, 'src/index.js'),  output: {    path: path.resolve(__dirname, 'dist'),    filename: '[name].[contenthash].js',  },  optimization: {    splitChunks: {      chunks: 'all',    },  },};

Webpack 4 尽力为你做到了最好,你不用通知它你怎么拆分你的 bundle。

这会导致一些话术比方:「哦,Webpack整挺好!」

还有许多相似于「你tm到底对我的 bundle 做了什么?!」的疑难。

铛铛!加上optimization.splitChunks.chunks = 'all'是一种「把所有 node_modules里的货色栋放到一个叫vendors~main.js的文件」的说法。

有了这个根本的 bundle splitting 之后,Alice 还会在每次拜访时下载一个新的200 KB的 main.js,但只会在第一周,第八周和第五周下载这个 200 KB 的 vendor.js

巧了,这两个 bundle 的大小恰好是200 KB。

总共 2.64 MB。

缩小36%,配置中增加了五行代码,还不赖。 在持续读这篇文章之前,当初就去实际下。 如果须要从Webpack 3 降级到4,不要放心,它十分无痛(而且收费!)。

我感觉这种性能晋升可能会更形象,因为扩散在十个星期,但其实交付给忠诚用户的字节数缩小了36%,咱们应该为本人感到骄傲。

但咱们还能够做得更好。

把每个 npm 包都拆出来

咱们的vendor.js遇到了与原来main.js文件雷同的问题——对其中一部分进行更改意味着须要从新下载它的全副。

所以为什么不为每个npm包独自筹备一个文件呢? 这很容易做到。

那让咱们将reactlodashreduxmoment等分成不同的文件:

const path = require('path');const webpack = require('webpack');module.exports = {  entry: path.resolve(__dirname, 'src/index.js'),  plugins: [    new webpack.HashedModuleIdsPlugin(), // 确保 hash 不被意外扭转  ],  output: {    path: path.resolve(__dirname, 'dist'),    filename: '[name].[contenthash].js',  },  optimization: {    runtimeChunk: 'single',    splitChunks: {      chunks: 'all',      maxInitialRequests: Infinity,      minSize: 0,      cacheGroups: {        vendor: {          test: /[\\/]node_modules[\\/]/,          name(module) {            //获得名称。例如 /node_modules/packageName/not/this/part.js            // 或 /node_modules/packageName            const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];            // npm package 是 URL 平安的,但有些服务不喜爱 @ 符号            return `npm.${packageName.replace('@', '')}`;          },        },      },    },  },};

这篇文档 会很好地阐明这里的大多数内容,然而我要向你解释一些乏味的局部,因为它们花了我巨长的工夫才弄对。

  • Webpack 领有一些聪慧但实际上有点傻的默认配置,像:宰割输入文件时最多能够蕴含3个文件,最小文件大小为30 KB(即比这小的文件都能够合并在一起)。 所以我笼罩了这些配置。
  • cacheGroups是咱们定义 Webpack 如何将 chunks 到输入到 bundle 的规定。 这里有一个叫做「vendor」的,任何node_modules的包都在这里。 通常,你只需将输入文件的名称定义为字符串。 然而我将name定义为一个函数(将为每个已解析的文件调用该函数),从模块门路返回包的名称。 后果是,每个包都会生成一个文件,比方 npm.react-dom.899sadfhj4.js
  • 发包时包名称必须是 URL平安的。所以咱们不用用encodeURI 解决 packageName 。然而,我发现 .NET服务无奈提供名称中带有@的文件(来自范畴限定的包),所以我在此代码段中替换了该命名。
  • 整个配置很棒,因为它是一劳永逸的。 它无需保护——我不须要按名称援用任何包。

这时,Alice 仍每周从新加载咱们200 KB的main.js文件;在首次拜访时仍将下载200 KB的 npm 包,但她永远不会再次下载雷同的包。

忽然发现每个 npm 包都恰好是20 KB。 世所常见!

这是2.24MB。

与比拟基准值相比缩小了44%,仅仅从博客文章中 copy/plate 了某些代码的你来说,这十分酷。

我在想会不会有可能超过50%?

那不是盖了帽了吗。

拆分利用的代码区域

让咱们关注下可怜的 Alice 一次又一次(甚至再一次)下载的main.js文件。

后面我提到过,咱们在此站点上有两个截然不同的局部:一个产品列表和一个产品详情页。 这些区域中每个区域的本身的代码为 25 KB(保留150 KB的共享代码)。

当初,咱们的“产品详情”页面变动不大,因为咱们做得如此完满。 因而,如果咱们将其分成独自的文件,则大多数时候都能够从缓存中提供。

对此,咱们应该搞些事件。

咱们手动地增加一些入口点,通知 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(), // 确保 hash 不被意外扭转  ],  optimization: {    runtimeChunk: 'single',    splitChunks: {      chunks: 'all',      maxInitialRequests: Infinity,      minSize: 0,      cacheGroups: {        vendor: {          test: /[\\/]node_modules[\\/]/,          name(module) {            //获得名称。例如 /node_modules/packageName/not/this/part.js            // 或 /node_modules/packageName            const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];            // npm package 是 URL 平安的,但有些服务不喜爱 @ 符号            return `npm.${packageName.replace('@', '')}`;          },        },      },    },  },};

靠谱的 Webpack还会为在 ProductList 和 ProductPage 之间共享的内容创立文件,使得咱们不会失去反复的代码。

大多数状况下,这将为敬爱的 Alice 省下额定的50 KB下载量。

咱们在第6周调整了 icon,我非常确定你还记得

这些仅仅1.815 MB!

咱们曾经为 Alice 节俭了高达56%的下载量,并且这种节俭将(在咱们的实践场景中)始终继续到工夫止境。

我之前提到过,在测试之下的理论状况并不重要。 这是因为,无论你遇到什么状况,论断都是雷同的:将你的利用拆分为正当的小文件,你的的用户就会下载更少的代码。


很快,我将要探讨「code splitting」——另一种类型的文件拆解——但首先,我想解决你当初在想的三个纳闷点。

1:有很多网络申请会不会很慢?

答案是很大声的“不”。

在 HTTP/1.1 时代已经是这种状况,然而在 HTTP/2 中却不是这种状况。

只管,这篇2016年的文章和Khan Academy 2015年的帖子都得出了这样的论断:即便应用HTTP/2,下载太多文件的速度依然较慢。 但在这两个帖子中,「太多」文件意味着「几百」。 因而,请记住,如果你有数百个文件,才可能会达到并发限度。

或者你想晓得追溯到 Windows 10上的 IE11 对 HTTP/2 的反对。我对应用比这更早版本的环境的人进行了详尽的考察,他们统一向我保障,他们不在乎网站加载的速度如何。

2:会不会 每个Webpack bundle 中都有开销/样板代码?

的确。

3:会不会因为领有多个小文件而丢失了压缩方面(的劣势)?

对,也的确会。

呃,糟了:

  • 更多的文件 = 更多 Webpack 样板代码
  • 更多的文件 = 更少的压缩

让咱们对它来一个量化,使得咱们确切晓得须要放心的水平。

……

OK,我刚做了一个测试,一个190 KB的站点分为19个文件,增加到发送给浏览器的总字节数中大概减少了2%。

所以……对于第一次拜访有2%的增量,与此同时在之后的拜访有 60% 缩小量,直到宇宙的止境。

因而咱们所须要放心的水平正确的是:齐全没有。

在测试 1 vs 19 个文件时,我在想我能够在一些不同的网络上应用它,包含HTTP/1.1。

而后上面是我搞的表格,强力撑持了“文件越多越好”的想法:

(从Firebase动态主机)加载的同一190 KB网站)

在 3G 和 4G 上,当有 19 个文件时,此站点的加载工夫缩小了 30%。

真的如此吗?

这是噪点十分多的数据。 例如,在 Run 2 的 4G 网络上,站点加载了646毫秒,而后两次运行则破费了 1116毫秒——在什么都没做的状况下长了73%。 因而,声称 HTTP/2 的速度进步了30%仿佛有点刁滑。

(行将推出:一种自定义图表类型,旨在可视化页面加载工夫的差别。)

我建了这张表,尝试量化 HTTP/2 的区别,但事实上我能惟一能说的是“可能没有多大区别”。

真正奇怪是最初两排。 那是旧的 Windows 和 HTTP/1.1,我打赌速度会慢很多,但并非如此。我想我需更慢的网络。


讲故事的工夫到了! 我从微软官网下载了Windows 7虚拟机来测试这些内容。

它附带了IE8,但我降级到IE9。

所以我转到了微软的IE9下载页面,而后……

我天。你在逗我?这还有什么好说哒

最初对于 HTTP/2 的最初一句话。你晓得它当初曾经集成到 Node 中了吗? 如果你想玩一下,我编写了一个带有 gzip,brotli 和响应缓存的100行HTTP/2 服务器,带来新的测试乐趣。


对于 bundle splitting 的所有就讲完了。 我感觉这种办法的惟一毛病就是必须一直压服人们置信加载许多小文件是OK的。


Code splitting(别加载你不须要的代码)

这种特地的办法只有在某些站点上才有意义,比方我的。

我想利用我刚想进去的 20/20 规定:如果你的网站的某个局部只有20%的用户拜访过,并且这个局部大过你站点 JavaScript 的20%,那么你只须要按需加载代码。

试着调整这些数字,显然,还有比这更简单的状况。 要害是要有一个阈值,能够确定 code splitting 对于你的网站到底合不合理。

怎么确定?

比方你有一个购物网站,并且想晓得是否应该将「领取」代码离开,因为只有30%的用户能够走到领取流程。

<mark>你须要做的第一件事是卖品质更好的货色。</mark>

第二件事,是计算出这块齐全惟一的代码量。 因为你在执行「code splitting」之前应该始终进行「bundle splitting」,所以你可能曾经晓得这部分代码有多少。

(它可能比你设想的要小,所以在你蠢蠢欲动之前先把代码量累加一下。比方你有一个 React 站点,那么你的 store,reduce,路由,actions 等都将在整个站点上共享。 齐全惟一的局部次要是一些组件和他们的工具。)

而后你留神到,领取页面齐全惟一的代码为 7 KB。 该网站的其余部分为 300 KB。 我看到这个会说,emm,我不会费劲去拆分这块的代码,有这么几个起因:

  • 事后加载它不会减慢速度。 记住你正在并行加载所有这些文件。 能够试试看记录 300 KB 和 307 KB之间的加载工夫差别。
  • 如果你想之后加载这块的代码,那用户在点击「把我的钱拿走」的时候必须期待这块的文件——这个时候正是你想要它最平滑的时候。
  • Code splitting 要求你改你的我的项目中的代码。 它引入了异步逻辑,而以前只有同步逻辑。 这不是什么造火箭的学识,然而也具备复杂性,因而我认为应该以可感觉到的用户体验改善为由(去做这件事)。

OK,以上就是所有的相似于「这项令人兴奋的技术可能不实用你」的派对败兴者的话术了。

咱们来看下两个 code-splitting 的例子……

Polyfills

我将从这里开始,因为它实用于大多数网站,并且是一个很好的简略入门。

我在网站上应用了许多花哨的性能,因而我有能够导入我须要的所有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(); // 对,目前这一行是无意义的

咱们应用 bundle spiltting 局部中的 Webpack config,因为这里有四个 npm 包,我的 polyfill 将被主动拆分为四个不同的文件。 它们总共约 25 KB,并且90%的浏览器不须要它们,所以值得动静加载。

应用 Webpack 4和 import() 语法(不要和没有括号的import语法搞混了),条件加载polyfill是非常容易的。

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);}

有情理吧? 如果所有这些货色都反对的话,就渲染页面。 否则导入polyfills,而后渲染页面。 当此代码在浏览器中运行时,Webpack 运行时将解决这四个 npm 包的加载,并且在下载并解析它们后,调用render() 并持续进行。

(顺便说一句,要应用import(),你将须要Babel的dynamic-import plugin。此外,正如Webpack文档所说,import()用了promises,你须要将这个polyfill与其余polyfill离开去polyfill。)

很简略,是吧?

上面是有些麻烦的例子……

基于路由的动静加载(React 独占)

回到 Alice 的例子,假如该网站当初有一个「治理」局部,商品销售者能够登录并治理他们想要发售的玩意。

这一块具备许多完满的个性,大量的图表以及 npm 中的大型图标库。 我曾经做了 bundle splitting,能够看到它们全副超过100 KB。

以后,我有一个路由设置,当用户查看/admin URL时将出现<AdminPage>。 当Webpack将所有内容打包在一起时,它将找到import AdminPage from './AdminPage.js',而后说“喂,我须要在初始有效载荷中包含它”。

然而咱们不心愿这样。 咱们须要把这个援用放在 admin 页面动静导入里,比方import('./AdminPage.js'),Webpack 就会晓得须要动静加载它。

很酷,不须要任何配置。

所以,我不不应该间接援用AdminPage,而是建另一个将在用户拜访/admin URL时出现的组件, 它可能看起来像这样:

import React from 'react';class AdminPageLoader extends React.PureComponent {  constructor(props) {    super(props);    this.state = {      AdminPage: null,    }  }  componentDidMount() {    import('./AdminPage').then(module => {      this.setState({ AdminPage: module.default });    });  }  render() {    const { AdminPage } = this.state;    return AdminPage      ? <AdminPage {...this.props} />      : <div>Loading...</div>;  }}export default AdminPageLoader;

这个概念很简略,对吧? 组件mount后(意味着用户位于/admin URL),咱们将动静加载./AdminPage.js,而后保留该组件的援用。

在 render 办法中,咱们仅仅简略地在期待<AdminPage>加载时渲染<div> Loading ... </ div>,在<AdminPage>加载实现存储它。

我只是出于好玩写了这个例子,实际上你只须要应用react-loadable来实现它,正如 React Code-Splitting 文档说的那样。


好啦,我想这就是全副了。有没有能够总结我下面说过的货色的句子,但用更少的文字?

  • 如果人们屡次拜访你的网站,就把你的的代码分成许多小文件。
  • 如果你的网站上有大部分用户不拜访的大块内容,动静加载这块代码。

感激浏览。祝你明天欢快~

靠,我忘了提 CSS 了。



本文由 Moltemort 翻译,转载本文请注明源链接