本文共 4841 字,大约阅读时间需要 16 分钟。
Fider(Fider是一款开源的产品反馈搜集平台)团队一直致力于优化应用程序的体验。作为一个使用React构建的web应用程序,他们主要关注如何减少JS和CSS代码的体积。在这篇文章中,Fider团队分享了他们的学习体会。通过掌握这些概念和建议,你也可以针对自己的Web应用做出类似的优化。
Fider的前端由React和Webpack构建,所以下面的主题非常适用于使用相同技术栈的前端团队,但是这些概念也可以应用于其他技术栈。Fider本身是一个开源项目,所以你实际上可以在项目地址上提交PR或者查看源码。
webpack-bundle-analyzer是一个Webpack插件,它可以将你所使用的Bundle生成为一个可交互、可缩放Treemap。通过这个插件,你可以看到Bundle中哪个Module占用体积最大。
如果你不知道产生问题的根本原因,你怎么能解决它呢?
下图是webpack-bundle-analyzer生成结果的示例。
在考虑Bundle体积前,先用webpack-bundle-analyzer进行分析,是一个很好的习惯。
Long term caching是指告知浏览器长时间缓存一个文件,比如3个月甚至1年。它之所以非常重要,是因为这项特性可以确保再次返回同一页面浏览的用户,不需要重复下载相同的JS/CSS文件。
浏览器是根据文件的完整路径名来缓存文件的。因此,假如你需要强制用户下载新版本的Bundle,首先必须重命名这个Bundle。幸运的是,Webpack提供了一个功能,可以根据一定规则来更新Bundle的名称,从而解决这个问题。
长久以来,Fider团队一直把Chunkhash作为Webpack编译输出的配置。但是在99%的需要Long Term Caching的场景中,最好的选择Contenthash。它将根据JS/CSS的内容生成散列,因此只有内容发生改变,Bundle的命名才会变化,从而尽可能的利用Long Term Caching的优势。
尽管这种技术不会减少Bundle的大小,但它确实有助于减少用户下载Bundle的时间。如果某个Bundle没有更改,用户就无需再次下载它。
更多相关信息可以从Webpack官方文档中获取:
长久以来,许多团队在实践中,都会将所有的NPM Package构建成一个单独的Bundle。这与Long Term Caching相结合时非常有用。
相比我们应用自身的代码,NPM Package的改动往往要小得多。因此我们可以把这些包单独打包成一个Bundle,从而避免用户重复下载这些NPM Package。这些由Npm Package构建成的Bundle通常称为Vendor Bundle。
但是我们可以做的更好。如果你自己也有一部分很少更改的代码呢?
也许你已经有了一些在一段时间之前创建,并且迄今为止很少修改的组件,如Button,Grid,Toggle 等,那么这些组件就可以作为Common Bundle的候选。你可以看看PR #636:
在这个PR中,我们就是把一些特定文件夹中的Module单独构建成了一个Common Bundle。
这将确保,除非我们更改这些公共组件,否则用户就不需要重新下载它们。Code Splitting是当前的热门话题。代码分割在过去不是一件简单的事,但是随着工具和框架的长足发展,现在进行Code Splitting相对而言简单得多。
一种常见的情况是,即使用户只是需要查看主页,应用程序也会推送一个包含所有JS/CSS的Bundle。这些JS/CSS可以用来渲染应用程序内的所有页面。因为我们已经推送了所有的JS/CSS代码,所以不用去关心用户是否会去访问站点内的其他页面,比如设置页面。Fider这么做已经很久了,但是现在我们已经做出了改变。
Code Splitting的思想是生成一个MainBundle以及多个更小的Bundle(通常一个路由对应一个)。我们唯一分发给所有用户的是Main Bundle,然后由Main Bundle异步下载当前页面渲染所需的Bundle。
这看起来很复杂,但是多亏了React和Webpack这对组合,Code Splitting已经不是什么难事了。对于React \u0026lt;= 16.5的用户,我们推荐来做Code Splitting。如果你已经在使用React 16.6,那么你可以使用response .lazy(),它是这个版本新增的特性。
通过使用Webpack Bundle Analyzer,我们注意到,在Vendor Bundle中引入了response-toastify。response-toastify是我们用于弹出消息提示的toaster库。在Fider中,我们很少弹出消息,那么把response-toastify直接引入Bundle就不是非常合理。假如用户并不需要显示弹出消息,那为什么要向他们推送这30kB的JavaScript代码呢?
这个问题和第四节的问题很相似,但是这里我们讨论不再是路由,这是在多个路由中使用的某个特性。你能在feature级别上进行Code Splitting吗?
是的,完全可以!
简而言之,你需要做的就是从静态导入切换到动态导入。
// beforeimport { toast } from \u0026quot;./toastify\u0026quot;;toast(\u0026quot;Hello World\u0026quot;);// afterimport(\u0026quot;./toastify\u0026quot;).then(module =\u0026gt; { module.toast(\u0026quot;Hello World\u0026quot;);});
这样,Webpack会将toastify模块及其NPM依赖项单独打包。之后浏览器就会在需要toast的时候下载对应的Bundle。如果您已经配置了Long term caching,那么在第二个toaster调用时就无需再次下载。下面这个动图就显示了这个过程。
你可以去PR #645上了解更多关于此功能的实现细节:
Tree Shaking是只指从Module中导入你需要的东西,然后丢弃其余部分的过程。Webpack在生产模式下时默认启用这个功能。
通常使用Font Awesome的方法是导入一个外部字体文件和一个CSS,然后将字体上的每个字符或者图标映射形成一个CSS类。带来的结果是,即使我们只是使用图标A、B和C,用户也要下载这个外部字体和一个包含600多个图标定义的CSS。
幸运的是,我们找到了response-icons,这是一个NPM Pakage,包含了SVG格式的Font Awsome和其他图标包!通过它,我们可以将这些图标导出成一个ES Module格式的React组件。
然后你可以只导入你需要的图标,Webpack会从Bundle中删除其他图标。结果呢?不仅我们的CSS文件小了近68kB,同时我们也不需要下载外部字体了。这是Fider团队在减少CSS体积上的最大贡献。
可以在这个PR中看到更多实现细节 PR #631:
“NPM就像一个乐高商店,里面装满了积木,你可以随意挑选你喜欢的。你不需要为安装Package付费。但是这些Package会占用应用程序的字节大小,你的用户会为此买单。所以请做出明智的选择。”
在使用Bundle Analyzer时,我们发现,单单markdown-it就占用了Vendor Bundle体积的40%。于是我们就想在NPM上寻找一个可替代的Markdown解析器,它必须更小、更好维护且能满足我们的需要。
在安装任意NPM Package之前,我们使用bundlephobia.com来分析它的大小。我们只需要修改少量API,就能从markdown-it切换到markit。这最终帮助Fider减少了大约63KB的Vendor Bundle的体积。
可以在这个PR中看到更多实现细节PR #643:
在添加大体积的Package之前,请三思。你真的需要它吗?您的团队有更简单的实现吗?如果没有,你能否找到另一个Package,能用更少的代码完成相同的工作? 如果还是不行,你可以像第5节使用react-toastify一样,首先添加这个NPM Package,然后根据需要进行异步加载。
假如你用路由级别的Code Splitting来创建一个应用,并且已经部署在生产环境上供用户使用了。假如现在你对Dashboard路由组件做出了更改,那么Webpack只会重新生成Dashboard 路由对应的那一份Bundle?是这样吗?
事实上并非如此。
如果应用程序代码发生某些改变,Webpack总是会重新生成Main Bundle。原因是Main Bundle是指向其他Bundle的指针。如果某个Bundle的散列值发生更改,那么Main Bundle也必须发生改变,以指向拥有新散列值的Dashboard Bundle。
因此,如果您的Main Bundle不仅包含这些指针,还包含许多常见组件,如按钮、切换、网格和选项卡。那么由于Main Bundle发生了变化,就会导致用户重复下载一些完全没有更改的东西。
因此,你首先需要使用Webpack Bundle Analyzer来看看你的Main Bundle中到底有些什么。然后再应用我们前面提到的一些技术来减少Main Bundle的体积。因为Main Bundle的优化至关重要。
将TypeScript代码编译成ES5时,TypeScript编译器会向最终输出的JavaScript文件添加一些辅助函数。这个过程确保,即使你的TypeScript中使用了某些旧浏览器不支持类(Classes ) 和生成器(Generators)等ES6特性,最后生成的代码也可与之相兼容。
在这种情况下,一旦在某个TypeScript文件中使用非ES5特性的语法,那么最后生成的文件就会包含这些Helper函数。虽然单独来看,这些Helper函数体积非常小,但是假如这些文件数量很多,那么这个空间消耗就不能被忽略了。由于Webpack无法对这些函数进行Tree Shaking,因此最终会生成一个略大的、包含多份相同的代码的Bundle。
幸运的是,一个名为tslib的NPM Package可以解决这个问题,它包含了所有TypeScript所需的Helper函数。在npm install tslib -save安装之后,我们通过在tsconfig.json上设置importHelpers: true,来让编译器从tslib包导入Helper函数,而不是重复添加。
对于一个React应用程序来说,如果大部分组件是由类构成,那么体积缩减就会非常明显。
你准备好迎接了吗?考虑一下你的应用程序的所有潜在用户,可能他们目前正试图用低成本设备、较慢的网络访问它。减少Bundle的大小将直接影响我们应用程序的性能,并让用户更好地访问它。
邱仁博,多年运营商商业分析、数据中心数据库方向工作经验,现任职于某地市图书馆信息技术部。日常关注国内外极客新闻、前后端技术。海外知识搬运工。
更多内容,可关注前端之巅(ID:frontshow)
转载地址:http://hytil.baihongyu.com/