[译]提升JavaScript 加载速度的10种方式

1,514 阅读8分钟

💡 原文地址

在许多现代网站中,有大量的JavaScript。事实上,根据HTTP统计,平均每个页面有超过500KB 的JavaScript。问题是,JavaScript的下载和解析都需要时间,这使得网站的加载速度大大降低,从而影响了网站的保留率,因为如果一个网站的加载时间太长,用户就会离开。幸运的是,有一些简单的方法可以减少你在网站上加载的JavaScript数量,并使你正在加载的JavaScript加载得更快,我们今天将介绍这些方法。

1. 懒加载

并不是所有的 JavaScript 都需要在用户第一次打开网站时加载。例如,你可能在页面底部有一个邮箱注册功能。除非用户浏览到这里,否则没必要去加载。因此,许多 Web 开发人员使用了一种懒加载的技术。相对于一次性加载所有的 JavaScript,懒加载只会加载当前会用到的部分 JavaScript。

懒加载有多种实现方式。例如,部分元素不需要立即就能交互,而是需要尽快的可以交互,你可以等到页面加载完后使用 requestIdleCallback()。或者像我们之前说的那样,如果有一个不许压迫立即交互的元素在页面底部,你可以使用 intersectionObserver 监听元素,等到用户浏览到这个元素时再加载。

现在问题是:如何让代码懒加载?最好的方式是:动态 import,它是 ESM 的一部分。使用 import() 可以让你在任何时候加载 JavaScript。例如,这将在浏览器空闲时加载一个 JavaScript:

requestIdleCallback(() => {
	import("/script.mjs");
});

另外一个实现懒加载的方式是:script 标签的 asyncdefer 属性。这个技术没有拓展性,但是它可以非常简单的实现当 DOM 加载完后在加载 JavaScript。

2. 最小化

最小化是提高性能的一个简单方法。它通常是通过Terser或ESBuild这样的自动最小化工具完成的。这些工具通过删除间距、长的变量名以及其他在开发中很有帮助但在生产中会增加脚本大小的东西来缩小你的代码。例如,假设我用Terser对这段代码进行了缩减:

window.addEventListener("DOMContentLoaded", (event) => {
	const images = document.getElementsByTagName("img");
	for (const image of images) {
		image.width = 50;
		image.height = 50;
	}
});

最后的结果为:

window.addEventListener("DOMContentLoaded", (e) => {
	const t = document.getElementsByTagName("img");
	for (const e of t) (e.width = 50), (e.height = 50);
});

这就是减少了67个字节,从203个字节减少到136个字节!这一点不会产生明显的差异,但对于较大的脚本,最小化可以产生相当大的影响。

3. Bundling

脚本大小不是唯一重要的事情。请求数也很重要,因为每个请求都会增加开销。基本上,你想把你的脚本数量保持在最低限度。然而,拆分代码通常是保持代码清洁的一种代码做法。幸运的是,像minifiers一样,有自动化的工具来解决这个问题。这些被称为捆绑器。捆绑器分析你的代码,查看哪些脚本在相互导入,并找出如何组合它们。最知名的捆绑器是Webpack、Rollup和Vite。

使用捆绑器的另一个好处是,大多数捆绑器也可以作为构建工具,使其很容易进行减化和TypeScript编译等工作。关于捆绑器的更多信息,请查看我关于捆绑器的文章。

4. 代码拆分

你可能会惊讶,这就在打包之后。"我打包我的代码只是为了把它拆开?"不一定。事实上,这也是打包工具的一个特点。虽然减少请求数是很好的,但你不希望用户不得不一次性加载你网站上的所有代码。你可以通过为每个页面创建一个新的完整的打包程序来解决这个问题,但这将否定缓存的一些好处(我们将在后面谈及)。为了解决这个问题,我们有代码拆分。代码拆分结合了打包和懒惰加载的优点,同时确保不加载页面的任何不必要的代码。打包工具通过分析导入的地图来执行代码分割,并找出哪些脚本需要放在自己的捆绑中。大多数打包工具都是自动完成的,尽管编写更容易分析的代码是有帮助的(例如,尽可能使用静态导入)。

5. Tree Shaking

捆绑器的另一个共同特征是树状摇晃。你可能会导入一个库的一部分,但不需要其他部分。然而,如果你在没有树状摇动的情况下这样做,最终用户将最终加载整个库,这可能会增加大量的JavaScript。树状摇动解决了这个问题;支持树状摇动的捆绑器会自动删除库中未使用的部分,从而大量减少你导入的代码。例如,看看Lodash(具体来说是lodash-es),一个大型的JavaScript工具库。整个模块几乎有100千字节,但如果你只使用intersect()函数,你将只导入2.7千字节的代码。现在,在Lodash的情况下,有一些包只包含单个函数,但如果你使用大量的函数,这些包使用起来会比较麻烦,很多库都不这样做。

6. ES 模块

为了使前面提到的许多功能发挥作用,ECMAScript Modules(ESM)是非常有用的,甚至是必不可少的。ESM是一个模块规范,是为了规范不同文件之间的代码共享方式而开发的。在ESM之前,有一些相互冲突的标准,如CommonJS和UMD,它们甚至不被浏览器原生支持。ESM统一了这些标准,并提供了有助于实现树状摇动等功能的语法(注意我在前面说的使用lodash-es而不是标准lodash)。此外,由于ESM在浏览器中被原生支持,你不需要一个沉重的polyfill就能使用ESM。

// ESM
import { something } from "test";
export const something = "test";
// CJS
const something = require("test").something;
module.exports.something = "test";

7. CDN

在你自己的服务器上托管静态文件是毫无意义的。使用一个完整的服务器进行实际的服务器端计算会增加你的成本、开发的复杂性和网站的加载时间。相反,CDN是更好的解决方案。CDN(内容交付网络)是一个服务器网络,旨在快速和廉价地提供静态文件。你可以从几十或几百个服务器(取决于CDN)提供文件,而不是只从一个服务器提供服务,这样可以减少延迟,因为服务器离用户更近。此外,CDN经常为你配置缓存和压缩等功能,节省时间。一些流行的CDN的例子是Cloudflare CDN和亚马逊的 CloudFront。

8. 缓存

虽然首次加载体验是至关重要的,但你也需要考虑到网站重复访问者的性能。使重复访问明显加快的一个方法是通过缓存。浏览器缓存的作用是保存网站资源的副本,并使用该副本而不是再次下载。这意味着重复访问的感觉几乎是即时的。要设置缓存,你需要在响应中为你要缓存的资源设置Cache-Control头。如果你使用的是CDN,这很可能是自动为你配置的。如果你没有,设置起来也很简单。

9. 压缩

我相信你已经遇到了.zip或.tag.gz文件。你可能也知道,在将目录转化为文件的同时,它们也减少了文件的大小。大小的减少是通过压缩完成的。压缩的工作原理是运行一种算法,通过缩小重复的语句和做一些其他事情来使文件变小,这取决于使用的算法。有许多流行的压缩算法,如deflate、lz4、Brotli和Zstandard。zip和gzipped文件使用的压缩算法是deflate。

实现压缩可能有点难,但也有简单的方法。最简单的方法是使用能自动压缩文件的CDN,正如我们在第7条谈到的。另一个实现压缩的简单方法是运行一个支持压缩的文件服务器。然而,如果你不能做到这两点,还有一些其他的解决方案。很多构建工具/捆绑器都有自动生成压缩形式的文件的插件,你可以将其提供给浏览器自动解压。浏览器用Accept-Encoding头告诉你它支持什么压缩算法,而你的服务器用Content-Encoding头告诉浏览器在响应中使用什么压缩算法。欲了解更多信息,请查看MDN关于HTTP压缩的文章。

10. Lighthouse

Lighthouse是一个帮助你自动审核网站性能的工具,同时还包括其他一些类别,如搜索引擎优化和可访问性。它对于发现性能问题并提供解决这些问题的便捷途径非常有帮助。如果你有Chrome或其他基于Chromium的浏览器,Lighthouse应该是默认可用的。如果你使用其他浏览器,你可以下载扩展程序或使用PageSpeed Insights。PageSpeed Insights还提供了来自真实用户的数据,如果你想了解用户的实际体验,这将会很有帮助。

总结

有了这些提示,你应该在你的网站上取得较大的性能收益,转化为更多的保留和转换。