寻找组织网络项目资料的最佳方法可能是一项艰巨的任务。 用户处理项目的场景有很多,需要考虑许多技术和其他因素。
该材料的作者(我们今天将其翻译发表)说,他想在这里告诉您为有效准备Web项目材料而需要了解的所有信息。 首先,它将是关于如何选择最适合特定项目及其用户的分离站点文件的策略。 其次,将考虑实施所选策略的方法。

一般资讯
根据
Webpack词汇表 ,有两种文件共享策略。 这是捆绑包拆分和代码拆分。 这些术语似乎可以互换使用,但不是。
- 拆分包是一种将大包分成几个部分(即较小文件)的技术。 在任何情况下,例如与单个捆绑包一起使用时,此类文件将由站点的所有用户下载。 该技术的优势在于改进基于浏览器的缓存机制的使用。
- 代码分离是一种涉及在需要时动态加载代码的方法。 这导致这样的事实,即用户仅在特定的时间点下载他需要与网站的特定部分一起工作的代码。
代码拆分似乎比代码拆分有趣得多。 而且,实际上,有一种感觉,在我们主题的许多文章中,主要重点是代码分离,该技术被认为是优化站点资料的唯一有价值的方法。
但是,我想说的是,对于许多站点而言,第一个更有价值的策略是捆绑的分离。 而且,也许所有的Web项目都可以从其实施中受益。
让我们详细讨论一下。
捆绑包的分离
拆分束的技术基于一个非常简单的想法。 如果您有一个巨大的文件,并且在其中更改了一行代码,那么普通用户下次访问该站点时将不得不下载整个文件。 但是,如果将此文件分为两个文件,则同一用户将仅需要下载已更改的文件,而第二个文件将从浏览器缓存中获取。
值得注意的是,由于通过拆分捆绑包来优化网站资料与缓存相关,因此初次访问该网站的用户无论如何都必须下载所有资料,因此这些资料将以单个文件还是多个文件的形式显示,对他们来说都没有区别。 。
在我看来,太多关于Web项目性能的讨论是专门针对首次访问该网站的用户的。 也许是这样,部分原因是该项目对用户产生的第一印象的重要性,以及用户首次访问该站点时传输给用户的数据量简单易行。
对于常规访客,可能很难衡量应用于他们的材料优化技术的影响。 但是,我们仅必须了解这种优化的后果。
要分析这些内容,您需要类似电子表格的内容。 您还需要创建严格的条件列表,在这些条件下我们可以测试每种研究的缓存策略。
这是一个符合上一段概述的脚本:
- 爱丽丝每周访问我们的网站10周。
- 我们每周更新一次网站。
- 每个星期,我们都会更新产品列表页面。
- 此外,我们还有一个页面,其中包含产品详细信息,但我们尚未对此进行处理。
- 在第五周,我们将新的npm包添加到项目材料中。
- 在第八周,我们更新了项目中已使用的npm软件包之一。
有些人(像我一样)将尝试使这种情况尽可能地切合实际。 但是您不需要这样做。 这里的实际情况并不重要。 为什么会这样-我们很快就会发现。
▍初始条件
假设我们的JavaScript包的总大小
main.js
400 Kb,并且在当前条件下,我们将所有这些作为单个
main.js
文件传输给用户。 我们有一个Webpack配置,通常来说,它类似于以下内容(我删除了与我们的对话无关的内容):
const path = require('path'); module.exports = { entry: path.resolve(__dirname, 'src/index.js'), output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash].js', }, };
当配置中只有一个条目时,Webpack会为生成的
main.js
文件命名。
如果您对使用缓存不是一个很好的主意,请记住,每次我在此处编写
main.js
,实际上是指诸如
main.xMePWxHo.js
类的
main.xMePWxHo.js
。 疯狂的字符序列是文件内容的哈希,在配置中称为
contenthash
。 使用此方法会导致一个事实,即在更改代码时,文件名也会更改,这将迫使浏览器下载新文件。
根据上述情况,当我们每周对站点代码进行一些更改时,软件包的
contenthash
行会更改。 结果,爱丽丝必须每周访问我们的网站,才能上传400 Kb的新文件。
如果我们制作一个不错的平板电脑(到目前为止结果行没有用),其中包含每个文件每周数据加载量的数据,那么我们将得到以下内容。
用户上传的数据量结果,事实证明用户在10周内下载了4.12 MB的代码。 该指标可以改进。
third将第三方软件包与主要代码分开
将大包装分为两部分。 我们自己的代码将在
main.js
文件中,而第三方代码将在
vendor.js
文件中。 这样做很容易,以下Webpack配置将帮助我们:
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试图使开发人员的生活尽可能轻松,因此他会尽其所能,并且不需要告诉他如何将捆绑包分成几部分。
程序的这种自动行为会带来一些乐趣,例如:“嗯,这个Webpack的魅力是什么”,以及精神上的许多问题:“我的捆绑包是做什么的?”。
无论如何,在配置配置中添加
optimization.splitChunks.chunks = 'all'
告诉Webpack,我们需要它从
node_modules
获取所有内容并将其放入
vendors~main.js
。
在对捆绑包进行了基本分离之后,每周定期访问我们网站的爱丽丝(Alice)每次访问时都会下载200 Kb的
main.js
文件。 但是她只会下载3次
vendor.js
文件。 这将在第一,第五和第八周的访问期间发生。 这是对应的表,根据命运的
main.js
,前四个星期
main.js
和
vendor.js
的大小重合且等于200 Kb。
用户上传的数据量结果,事实证明,用户在10周内下载的数据量为2.64 MB。 即,与束分离之前相比,体积减小了36%。 通过在配置文件中添加几行,不会带来如此糟糕的结果。 顺便说一句,在继续阅读之前-在您的项目中做同样的事情。 而且,如果您需要从Webpack 3升级到4,则无需担心,因为此过程非常简单并且仍然免费。
在我看来,这里考虑的改进看起来有些抽象,因为它已经持续了10周。 但是,如果考虑发送给忠实用户的数据量,那么这实际上减少了36%。 这是一个很好的结果,但是可以改进。
▍在单独的文件中突出显示包
vendor.js
文件与原始
main.js
相同的问题
main.js
它包含以下事实:更改此文件中包含的任何软件包都会导致普通用户需要再次下载整个文件。
为什么我们不为每个npm包创建单独的文件? 这样做并不难,所以让我们将
react
,
lodash
,
redux
,
moment
等分解为单独的文件。 以下Webpack配置将帮助我们:
const path = require('path'); const webpack = require('webpack'); module.exports = { entry: path.resolve(__dirname, 'src/index.js'), plugins: [ new webpack.HashedModuleIdsPlugin(), // ], 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- , , // URL, @ return `npm.${packageName.replace('@', '')}`; }, }, }, }, }, };
在
文档中,您可以找到有关此处使用的构造的出色解释,但是我仍然花一些时间来讲述一些事情,因为正确地使用它们花费了很多时间。
- Webpack具有相当合理的标准安装,但实际上并不是那么合理。 例如,最大输出文件数设置为3,最小文件大小为30 KB(即,较小的文件将被合并)。 我重新定义了它。
cacheGroups
是我们设置Webpack如何将输出文件中的数据分组的规则的地方。 我这里有一个vendor
组,它将用于从node_modules
加载的任何模块。 通常,输出文件的名称以字符串形式给出。 但是我给函数起了name
,每个处理过的文件都会被调用。 然后,我从模块路径中获取包名称。 结果,我们为每个软件包得到一个文件。 例如, npm.react-dom.899sadfhj4.js
。- 软件包名称必须适合在URL中使用,以便它们可以在npm中发布,因此我们无需对
packageName
名称执行encodeURI
操作。 但是,我遇到了一个问题,.NET服务器拒绝使用其名称中包含@
符号的文件(此类名称用于具有给定范围名称的程序包,即所谓的作用域程序包),因此,代码片段,我摆脱了此类字符。
上面的Webpack配置很好,因为您只需配置一次,然后就不用管它了。 它不需要按名称引用特定的程序包,因此,即使在更改程序包组成时,它仍然是相关的。
我们的常客Alice仍然每周
main.js
200千字节的
main.js
,并且在她第一次访问该网站时,她被迫下载200 KB的npm软件包,但是不必两次下载相同的软件包。
下面是该表的新版本,其中包含有关每周数据下载量的信息。 碰巧的是,带有npm软件包的每个文件的大小为20 Kb。
用户上传的数据量现在,在10周内下载的数据量为2.24 Mb。 这意味着我们已将基本税率提高了44%。 结果已经很不错了,但是问题在于是否有可能这样做以达到超过50%的结果。 如果发生这种情况,那就太好了。
application将应用程序代码分成片段
我们返回到
main.js
文件,不幸的是,Alice必须不断下载该文件。
正如我上面所说,我们的网站上有两个单独的部分。 第一个是产品列表,第二个是包含有关产品详细信息的页面。 每个代码唯一的代码大小为25 Kb(并且在那里使用150 Kb代码)。
产品信息页面不会更改,因为我们已经对其进行了完善。 因此,如果我们将其代码提取到一个单独的文件中,则在大多数情况下,该文件将与网站一起使用,将从缓存中下载到浏览器中。
此外,事实证明,我们有一个巨大的内置SVG文件用于图标渲染,该文件重达25 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(), // ], 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- , , // URL, @ return `npm.${packageName.replace('@', '')}`; }, }, }, }, }, };
努力工作的Webpack还将为常用的文件创建文件,例如
ProductList
和
ProductPage
,即不会有重复的代码。
我们所做的一切几乎可以使Alice每周节省50 Kb的流量。 请注意,我们在第六周编辑了图标描述文件。 这是我们的传统餐桌。
用户上传的数据量现在仅用十个星期,仅下载了1.815 MB的数据。 这意味着流量节省高达56%。 根据我们的理论方案,普通用户将始终以这种节省水平工作。
由于对Webpack配置进行了更改,所有这些操作均已完成。 我们没有更改应用程序代码来实现这样的结果。
在前面,我谈到了这样一个事实,即进行这种测试的特定场景实际上并没有发挥特殊作用。 之所以这样说是因为,无论使用哪种方案,我们所讨论的一切结论都是相同的:将应用程序拆分成对应用程序有意义的小文件,这样就可以减少站点数据量,由其常规用户加载。
很快我们将开始讨论代码分离,但是首先我想回答您可能正在思考的三个问题。
▍问题编号1。 是否需要执行许多请求不会损害网站的加载速度?
您可以对这个问题做一个简单的简短回答:“不,它没有害处。” 在过去,当使用HTTP / 1.1协议以及使用HTTP / 2时,类似的情况也会导致问题,这不再相关。
尽管应注意,在2016年发表的
这篇文章以及2015年可汗学院的这篇文章中得出的结论是,即使使用HTTP / 2,使用太多文件也会减慢下载速度。 但是在这两种材料中,“太多”表示“几百”。 因此,值得记住的是,如果必须处理数百个文件,则对并行数据处理的限制可能会影响其下载速度。
如果您有兴趣,可以在Windows 10的IE 11中获得HTTP / 2支持。此外,我对使用较旧系统的用户进行了全面研究。 他们一致表示,他们的网站加载速度并不特别令人担忧。
▍问题编号2。 Webpack捆绑包具有帮助程序代码。 它会在系统上产生额外的负载吗?
是的,是的。
▍问题编号3。 处理许多小文件时,其压缩级别会降低,对吗?
是的,这也是事实。 实际上,我想这样说:
- 更多文件意味着更多Webpack帮助程序代码。
- 更多文件意味着更少的压缩。
让我们弄清楚,以了解这有多糟。
我刚刚进行了一项测试,其中将190 Kb文件中的代码分为19部分。 这使发送到浏览器的数据量增加了大约2%。
结果,事实证明,在第一次访问该网站时,用户将上传2%的数据,而在以后的访问中,该数据将减少60%,并且这种访问将持续非常长的时间。
那么值得担心吗? 不,不值得。
在比较使用1个文件的系统和包含19个文件的系统时,我使用各种协议(包括HTTP / 1.1)对其进行了测试。 下表强烈支持拥有更多文件意味着更好的想法。
关于使用Firebase静态托管托管的2个版本的网站的数据,其代码大小为190 Kb,但是,在第一种情况下,打包为1个文件,在第二种情况下,打包为19个文件在3G和4G网络中工作时,下载包含19个文件的网站所花的时间比下载包含一个文件的网站所花的时间少30%。
表中显示的数据中有很多噪声。 例如,通过4G(表中的“运行2”)下载站点的一个会话花费了646毫秒,而另一次(“运行4”)则花费了1116毫秒,这延长了73%。 因此,有一种感觉说HTTP / 2“快30%”是不诚实的。
我创建此表是为了了解HTTP / 2的用途。 但是,实际上,在这里唯一可以说的是,使用HTTP / 2可能不会特别明显地影响页面加载。
该表中的最后两行确实令人惊讶。 这是不是最新版本的Windows IE11和HTTP / 1.1的结果。 如果我试图提前预测测试结果,那么我肯定会说,这种配置将比其他配置加载材料慢得多。 没错,这里使用了非常快速的网络连接,而对于此类测试,我可能应该使用更慢的速度。
现在,我将告诉您一个故事。 为了在一个非常古老的系统上浏览我的网站,我从Microsoft
网站下载了Windows 7虚拟机。 IE8安装在那里,我决定升级到IE9。 为此,我转到了旨在下载IE 9的Microsoft页面。但是我做不到。
真倒霉...顺便说一下,如果我们谈论HTTP / 2,我想指出这个协议已经集成到Node.js中。 如果要尝试,可以使用我编写的支持响应缓存的小型
HTTP / 2服务器 gzip和brotli。
也许,我说过关于捆束分离方法的一切。 我认为,使用这种方法时,用户必须上传大量文件的唯一缺点实际上并不是这样的“缺点”。
现在让我们谈谈代码分离。
代码分离
代码拆分技术的主要思想是:“不要下载不必要的代码。” 有人告诉我,使用这种方法仅对某些站点有意义。
在代码分离方面,我更喜欢使用我刚刚制定的20/20规则。 如果仅20%的用户访问了网站的某些部分,并且该网站的JavaScript代码的20%以上提供了其功能,则仅应根据要求下载此代码。
当然,这些不是绝对数字,可以根据特定情况进行调整,实际上,存在比上述情况复杂得多的方案。 这里最重要的是平衡,如果这对您的网站没有意义,则完全不使用代码分离是完全正常的。
▍是否分开?
如何找到是否需要代码分离的答案? 假设您有一个在线商店,并且您正在考虑是否将其余代码与用于接收客户付款的代码分开,因为只有30%的访问者从您那里购买商品。
我能说什么 首先,您应该努力填充商店并出售一些东西,这对于更多的站点访问者来说是有趣的。 其次,您需要了解在接受付款的网站部分中完全唯一的代码数量。 由于您应该始终在“代码拆分”之前执行“捆绑包拆分”,并且希望这样做,因此您可能已经知道我们感兴趣的代码的大小。
也许这段代码可能比您想像的要小,所以在为优化网站的新机会而高兴之前,您应该安全地计算所有内容。 例如,如果您有一个React站点,那么该站点的所有部分都将共享存储库,reducer,路由系统,操作。 站点代码不同部分的唯一性将主要由它们的组件和辅助功能表示。
因此,您发现用于支付购买费用的站点部分的完全唯一代码需要7 Kb。 其余站点代码的大小为300 Kb。 在这种情况下,由于以下几个原因,我不会进行代码分离:
- 如果提前下载这些7 Kb,则该站点不会变慢。 请记住,文件是并行下载的,并尝试测量下载300 Kb和307 Kb代码所需的差异。
- 如果以后下载此代码,则用户必须在单击“付款”按钮后等待。 这是您需要一切都尽可能顺利进行的时刻。
- 代码分离要求对应用程序进行更改。 在代码中,凡事之前都是同步完成的,就会出现异步逻辑。 , , , , , .
, , , .
.
▍
, , , .
. , . :
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(); // ,
Webpack , , npm-. 25 , 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); }
, , , — . —
render()
. , Webpack npm-, ,
render()
.
,
import()
Babel
dynamic-import . , Webpack,
import() , .
, . .
▍ React,
. , , , .
, npm- . , , 100 .
, , URL
/admin
,
<AdminPage>
. Webpack ,
import AdminPage from './AdminPage.js'
.
. , ,
import('./AdminPage.js')
, Webpack , .
, .
, ,
AdminPage
, , URL
/admin
. , :
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;
. (, URL
/admin
),
./AdminPage.js
, .
render()
,
<AdminPage>
,
<div>Loading...</div>
,
<AdminPage>
, .
,
react-loadable
,
React .
总结
, , (, , CSS). :
亲爱的读者们! ?