最好先了解你要处理的内容。盘点出所有资源的清单( JavaScript 、图片、字体、第三方脚本和页面上开销较大的模块,例如轮播、复杂的信息图和多媒体内容),然后将它们按组细分。
可以根据以下方式设置分组。定义针对旧版浏览器的基本核心体验(即完全可访问的核心内容),针对功能强大的浏览器的增强体验(即丰富的完整体验)和额外体验(不是绝对必需的并且可以延迟加载的资源,例如网络字体、不必要的样式、轮播脚本、视频播放器、社交媒体按钮、大图片)。不久前,我们发表了一篇文章“ 改善 Smashing 杂志的性能 [2] ”,其中详细介绍了这些内容。
我们要根据优先事项来优化性能。首先加载核心体验,然后加载增强体验,最后加载额外体验。
还记得优秀的 cut-the-mustard [3] 技术吗?该技术将核心体验发送到旧版浏览器并将增强体验发送到现代浏览器的。该技术的更新版本[4]可以使用 ES2015+ < script type="module">,因此也被称为 module/nomodule 模式[5] 。
正如 Philip Walton 所写的那样,“该技术使用打包和编译工具来生成两个版本代码,一个版本使用现代语法(通过 < script type="module" > 加载),另一个使用 ES5 语法(通过 < script nomodule > 加载)。” 现代浏览器会将脚本解释为 JavaScript 模块并按预期运行,而旧版浏览器则无法识别该属性并忽略该属性,因为该属性是未知的 HTML 语法。这样我们可以向支持模块的浏览器发送更少的代码,现在大多数框架和 CLI 都支持它。
但有一点警告提示:module/nomodule 模式可能会在某些客户端上适得其反,因此你可能需要考虑使用 Jeremy 提出的的低分险差分服务模式[6],但是这种模式不能使用预加载扫描程序,可能会以人们无法预料的方式影响性能。(谢谢 Jeremy !)
实际上,Rollup 支持以模块作为输出格式,因此我们既可以打包代码,又可以在生产环境中部署模块。Parcel 刚刚在 Parcel2 中添加了模块支持。Webpack 目前还没有完全实现该功能。另外,可以关注下 Pika[7],它正在考虑简化对 JavaScript 模块的管理和构建过程。
注意:值得指出的是,仅靠功能检测还不足以正确作出决定将那种体验发给浏览器。就其本身而言,我们无法从浏览器版本推断出设备的能力。例如,发展中国家的廉价 Android 手机大多运行 Chrome,尽管它们的内存和 CPU 能力有限,但也能达到检测要求。
最后,使用“ 客户端内存提示 HTTP 报文头”[8],我们可以更可靠地定位低端设备。在撰写本文时,该报文头仅在 Blink 中得到支持。自从 Chrome 浏览器提供一个设备内存相关的 JavaScript API 后,还可以选择基于 JavaScript API 去检测低端设备,如果不支持该功能,则可以使用 module/nomodule 技术(谢谢,Yoav!)。
tree-shaking[9] 是一种清理构建产物的方法,它让构建结果只包含在生产中实际使用的代码,并消除 Webpack 中未使用的引入。借助 Webpack 和 Rollup,我们还可以实现 scope hoisting [10],这两个工具都可以检测到 import 链可以在哪个位置打平并转换为一个内联函数,而不破坏代码。借助 Webpack,我们还可以使用 JSON Tree Shaking[11]。
另外,你可能需要考虑学习如何避免过时和昂贵的样式[12]。如果想要做的更好,你还可以使用 Webpack 缩短类名,并在编译时在独立作用域范围内动态重命名 CSS 类名[13]。
code-spliting[14] 是 Webpack 的另一个功能,可将你的代码拆分为按需加载的“块”。并非所有 JavaScript 都必须立即下载、解析和编译。一旦在代码中定义了分割点,Webpack 就可以处理依赖关系和输出文件。它可以让浏览器保持较小的初始下载量,并在应用程序请求时按需请求代码。Alexander Kondrov 对 Webpack 和 React 的 code-spliting[15] 有一个很棒的介绍。
考虑使用 preload-webpack-plugin[16],该插件根据你代码的分隔方式,让浏览器使用 <link rel="preload"> 或 <link rel="prefetch"> 对分隔的代码“块”进行预加载。Webpack 内联指令[17]还可以对 preload/prefetch 进行一些控制(但是请注意优先级问题[18]。)
在哪里定义分割点?可以通过跟踪页面使用了哪些 CSS / JavaScript 块,哪些未使用来决定。Umar Hansa 解释了[19]如何使用 Devtools 的 Code Coverage 来确定分割点。
如果你不使用 Webpack,请注意 Rollup 输出的结果明显好于 Browserify 的输出。与此同时,你可能想看看 rollup-plugin-closure-compiler[20] 和 Rollupify[21],它们将 ECMAScript 2015 模块转换为一个大的 CommonJS 模块,这么做是因为基于你选择的打包程序和模块系统对小模块的打包成本出奇的高[22]。
在处理单页面应用程序时,我们需要一些时间来初始化应用程序,然后才能渲染页面。这些设置需要你自己的解决方案,但你可以注意模块选择和使用一些技术以加快初始呈现时间。例如,如何调试 React 性能[23]和消除常见 React 性能问题的方法[24],还有改善 Angular 性能的方法[25]。通常,大多数性能问题来自启动应用程序的初始化时间。
那么,最好的代码分割方式是什么?Phil Walton 表示,“除了对动态导入的代码进行分割外,我们还可以包级别对代码进行分割,对于每一个引入的 node 模块基于包名单独打包到一个’块‘中。” Phil 还提供了具体如何做的教程[26]。
为了减少对可交互时间的负面影响,最好考虑将繁重的 JavaScript 抽离到 Web Worker 中或通过 Service Worker 进行缓存。
随着代码库的不断增长,UI 性能瓶颈将会显现出来,从而使用户体验变慢。这是因为 DOM 操作是与 JavaScript 一起运行在主线程上[27]。使用 Web worker[28],我们可以将这些昂贵的操作转移到后台其他线程上运行。Web Worker 的典型使用场景是预加载数据和渐进式 Web 应用程序[29],这种方式可以预先加载和存储一些数据,以便后续在需要时使用它。而且,你可以使用 Comlink[30] 来简化主页面和 Web Work 之间的通信。虽然这种方式还不完善,但我们正在一步步实现。
如何开始?这里有一些值得研究的资源:
请注意,Web Workers 无法访问 DOM,因为 DOM 不是“线程安全的”,并且执行的代码需要包含在单独的文件中。
我们可以将繁重的计算任务抽离[35] 到 WebAssembly[36](WASM)执行,它是一种二进制指令格式,被设计为一种用高级语言(如 C / C ++ / Rust)编译的可移植的对象。它对 浏览器的支持非常出色[37],并且随着 JavaScript 和 WASM 之间的函数调用变得越来越快[38],使它最近变得切实可行。另外,它甚至在 Fastly 的边缘云中也受支持[39]。
当然,WebAssembly 的目的并不是替代 JavaScript,但可以在你发现 CPU 占用过高时作为 JavaScript 的补充。对于大多数 Web 应用程序来说,JavaScript 更适合,而 WebAssembly 最适合用于计算密集型 Web 应用程序,例如 Web 游戏。
如果你想了解有关 WebAssembly 的更多信息:
图片
Milica Mihajlija 概述了WebAssembly 的工作原理以及其有用性[46]。
确保使用一些提前编译[47]来将一些客户端渲染移交到服务端[48],这样能更快更少的输出有用结果到客户端。最后,考虑使用 Optimize.js 来加快初始加载速度,它的原理是包装优先级高的调用函数(虽然现在已经没什么必要了)。
由于 ES2015 在现代浏览器中得到了很好的支持[49],因此我们可以使用 babel-preset-env 只转换你的代码中现代浏览器不支持的 ES2015+ 部分。然后设置两个版本[50],一个使用 ES6,一个使用 ES5。如上所述,所有主要浏览器现在都支持[51] JavaScript 模块,因此,对支持 ES 模块的浏览器使用 [script type="module"](https://developers.google.com/web/fundamentals/primers/modules "script type="module"") 加载文件,而较老的浏览器可以使用 script nomodu 加载旧版本文件。我们可以使用 Webpack ESNext Boilerplate[52] 来自动实现整个过程。
请注意,如今,我们能以模块化方式编写 JavaScript,这种 JavaScript 可以在在浏览器直接运行,而无需使用编译或打包程序。[<link rel="modulepreload"> 标签](https://developers.google.com/web/updates/2017/12/modulepreload "<link rel="modulepreload"> 标签")提供了一种提前加载(且优先级高)模块脚本方法。基本上,通过告诉浏览器需要加载的内容使浏览器在长时间网络往返过程中不会被任何事情阻碍,这是最大化使用带宽的一种好方法。此外,Jake Archibald 还发布了一篇很值得一读的关于 ES 模块的注意实现和陷阱[53]的文章。
对于 lodash,使用 babel-plugin-lodash[54] 只加载你在源代码中使用的模块。你的依赖关系可能还取决于 Lodash 的其他版本,因此将一般 requrie 方式改为单个模块引入方式[55]可以避免代码重复。这可以为你节省很多 JavaScript 负载。
Shubham Kanodia 编写了一份详细的低维护的智能打包指南[56]:你可以正确使用这种方式将老旧代码代码通过代码片段只发送给老版的浏览器。
pic
Jake Archibald 发布了一篇详细的文章,介绍关于 ES 模块的注意实现和陷阱[57],例如,内联的模块脚本推迟到阻塞的外部脚本和内联脚本执行之后执行。
我们只想通过网络发送必要的 JavaScript,但这意味着对这些资源的交付要更加专注和细致。不久前,Philip Walton 引入了 module/nomodule[58] 的思想(也就是 Jeremy Wagner 提出来的差分服务[59])。这个想法是编译并提供两个单独的 JavaScript 包:“常规”构建的构建方式是,一个包含 Babel 转换和 polyfills,仅提供给实际需要它们的旧版浏览器,另一个包(相同功能)不包含 Babel 转换和 polyfills。
因此,我们通过减少浏览器需要处理的脚本数量来帮助减少主线程的阻塞。Jeremy Wagne 发表了一篇关于差分服务以及如何在你的构建流中设置它的综合性文章[60],从 “Babel 设置”到“需要在 Webpack 中进行哪些调整”,以及“完成所有这些工作的好处”文章中都有涉及。
长期存在的项目会有尘封代码和过时代码越积越多的趋势。重新审视你项目的依赖并评估重构或重写最近引起问题的旧代码需要多少时间。当然,这总是一项艰巨的任务,但是一旦你了解了遗留代码的影响,就可以从增量解耦[61]开始进行解决。
首先,设置指标来跟踪遗留代码:调用的比率是保持不变还是下降,而不是上升。公开鼓励团队不要使用这个库,并确保 CI 在这个库收到拉取请求时向开发人员发出警报。polyfills 可以帮助使用了标准浏览器特性的老旧代码过渡到重写的代码。
Chrome 中的 CSS 和 JavaScript 代码覆盖率工具[62]可以使你了解哪些代码已执行或应用,哪些未执行。你可以启动一个覆盖率检查,在页面上执行操作,然后查看覆盖率结果。一旦检测到未使用的代码,找出那些模块并使用 import() 延迟加载[63](请参阅整个过程)。然后重复代码覆盖率检查确认现在在初始化时加载代码有变少。
你可以使用 Puppeteer[64] 以编程方式收集代码覆盖率,[65]而 Canary 已经允许你 导出代码覆盖率结果[66]。正如 Andy Davies 指出的那样,你可能想要收集现代和旧式浏览器的代码覆盖率[67]。
Puppeteer 还有许多其他用法[68],例如,自动视觉对比[69]或在每次构建时监视未使用的 CSS[70]。如果你正在寻找有关 Puppeteer 的详细指南,Nitay Neeman 对 Puppeteer 进行了非常全面的概述[71],并提供了示例和用例。
此外,purgecss[72],UnCSS[73] 和 Helium[74] 可以帮助你从 CSS 中删除未使用的样式。如果不确定某个地方是否使用了可疑的代码,可以遵循 Harry Roberts 的建议[75]:为特定的类创建 1×1px 的透明 GIF 并将其放入一个 dead/ 目录中,例如 /assets/img/dead/comments.gif。之后,你将该图像设置为 CSS 中特定选择器的背景,如果该图片的访问记录出现在日志中就再等待几个月,如果没有出现,则表示没有人在其屏幕上出现过该旧组件:你可能可以进一步将其全部删除。
对于激进一点的部门,你甚至可以通过 监测 DevTools 使用 DevTools [76]在一组页面中自动收集未使用的 CSS。
正如 Addy Osmani 所指出的那样[77],当你只需要 JavaScript 库的一小部分时,你很有可能会加载整个 JavaScript 库,同时还会为不需要它们的浏览器提供过时的 polyfill,或者只是复制代码。为了避免以上问题,请考虑使用 webpack-libs-optimizations[78] 在构建过程中删除未使用的方法和 polyfills。
也将包审核添加到你的日常工作流程中。使用更小巧轻便的库替换你可能在几年前添加的一些大型的库,例如 Moment.js 可以替换原生的 Internationalization API[79],date-fns[80] 或 Luxon[81]。BenediktRötsch 的研究表明[82],从 Moment.js 切换到 date-fns 可能会为 3G 和低端手机上的 First Paint 节省大约 300ms 的时间。
诸如 Bundlephobia[83] 之类的工具可以帮助你了解添加一个 npm 包的代价。size-limit[84] 不仅会包检查大小,还会展示 JavaScript 的执行时长。你甚至可以将这些成本与 Lighthouse Custom Audit 集成在一起[85]。修剪包大小也适用于框架。通过删除或修整 Vue MDC 适配器[86](Vue 的物料组件),样式从 194KB 减少到 10KB。
是不是觉得有点冒险?你可以查看 Prepack[87]。它可以将 JavaScript 编译为等效的 JavaScript 代码,但是与 Babel 或 Uglify 不同,它可以让你编写普通的 JavaScript 代码,输出运行速度更快的等效 JavaScript 代码。
避免加载整个框架,你甚至可以修剪框架并将其编译到一个不依赖其他代码的原生 JavaScript 包中。Svelte 做到了[88],Rawact Babel 插件[89]也做到了,该插件在构建时将 React.js 组件转换为本地 DOM 操作。为什么?好吧,正如维护人员所解释的那样,“react-dom 包括每个可能渲染的组件或 HTML 元素的代码,包括用于增量渲染、计划、事件处理等的代码。但是有些应用程序并不需要所有这些功能(在页面初始化的时候)。对于此类应用程序,使用原生 DOM 操作来构建交互式用户界面可能会更好。”
pic
size-limit [90]还提供基本的包大小检查,以及有关 JavaScript 执行时间的详细信息。
pic
BenediktRötsch 在他的文章中[91]表明,从 Moment.js 切换到 date-fns 可能会为 3G 和低端手机上的 First paint 节省大约 300ms 的时间。
我们可以使用预测方式来决定何时预加载 JavaScript 块。Guess.js[92] 是一组工具和库,它们使用 Google Analytics 数据来确定用户最有可能访问从给定页面中的哪个页面。根据从 Google Analytics 或其他来源收集的用户导航模式,Guess.js 构建了机器学习模型,用以预测并预加载每个后续页面上所需的 JavaScript。
因此,每个交互元素都有一个参与的概率分数,客户端脚本基于该分数决定提前预加载资源。你可以将该技术集成到你的 Next.js 应用程序[93],Angular 和 React 中[94],并且有一个Webpack 插件[95]可以自动执行设置过程。
显然,你可能会让浏览器获取不需要的数据并预加载不需要的页面,因此好的做法是对预加载的请求数量做好控制。比如预取在检查出来的脚本中经过确认的,或者在关键的动作调用进入可视区域时进行推测性预取。
需要一些不太复杂的东西吗?DNStradamus[96] 会对 a 标签出现在可视区时对 DNS 进行预取。Quicklink [97] 和 Instant.page[98] 是小型库,它们在空闲时间自动在视口中预取链接,以尝试加快下一页导航的加载速度。Quicklink 会考虑到数据量,因此它不会在 2G 或 Data-Saver 启用状态下预取,Instant.page 在将模式设置为使用 viewport prefetching(默认设置)时也是如此。
[1]
https://www.smashingmagazine.com/2020/01/front-end-performance-checklist-2020-pdf-pages: https://www.smashingmagazine.com/2020/01/front-end-performance-checklist-2020-pdf-pages
[2]
改善 Smashing 杂志的性能 : https://www.smashingmagazine.com/2014/09/improving-smashing-magazine-performance-case-study/
[3]
cut-the-mustard : https://www.filamentgroup.com/lab/modernizing-delivery.html
[4]
更新版本: https://snugug.com/musings/modern-cutting-the-mustard/
[5]
module/nomodule 模式: https://philipwalton.com/articles/using-native-javascript-modules-in-production-today/
[6]
低分险差分服务模式: https://jeremy.codes/blog/a-less-risky-differential-serving-pattern/
[7]
Pika: https://www.pika.dev/blog/pika-web-a-future-without-webpack/
[8]
客户端内存提示 HTTP 报文头”: https://github.com/w3c/device-memory
[9]
tree-shaking: https://developers.google.com/web/fundamentals/performance/optimizing-javascript/tree-shaking/
[10]
scope hoisting : https://medium.com/webpack/brief-introduction-to-scope-hoisting-in-webpack-8435084c171f
[11]
JSON Tree Shaking: https://react-etc.net/entry/json-tree-shaking-lands-in-webpack-4-0
[12]
避免过时和昂贵的样式: https://benfrain.com/css-performance-revisited-selectors-bloat-expensive-styles/
[13]
动态重命名 CSS 类名: https://medium.freecodecamp.org/reducing-css-bundle-size-70-by-cutting-the-class-names-and-using-scope-isolation-625440de600b
[14]
code-spliting: https://webpack.js.org/guides/code-splitting/
[15]
Webpack 和 React 的 code-spliting: https://hackernoon.com/lessons-learned-code-splitting-with-webpack-and-react-f012a989113
[16]
preload-webpack-plugin: https://github.com/GoogleChromeLabs/preload-webpack-plugin
[17]
Webpack 内联指令: https://webpack.js.org/guides/code-splitting/#prefetching-preloading-modules
[18]
优先级问题: https://andydavies.me/blog/2019/02/12/preloading-fonts-and-the-puzzle-of-priorities/
[19]
解释了: https://vimeo.com/235431630#t=11m37s
[20]
rollup-plugin-closure-compiler: https://github.com/ampproject/rollup-plugin-closure-compiler
[21]
Rollupify: https://github.com/nolanlawson/rollupify
[22]
小模块的打包成本出奇的高: https://nolanlawson.com/2016/08/15/the-cost-of-small-modules/
[23]
如何调试 React 性能: https://building.calibreapp.com/debugging-react-performance-with-react-16-and-chrome-devtools-c90698a522ad
[24]
消除常见 React 性能问题的方法: https://blog.logrocket.com/death-by-a-thousand-cuts-a-checklist-for-eliminating-common-react-performance-issues/
[25]
改善 Angular 性能的方法: https://www.youtube.com/watch?v=p9vT0W31ym8
[26]
教程: https://philipwalton.com/articles/using-native-javascript-modules-in-production-today/
[27]
DOM 操作是与 JavaScript 一起运行在主线程上: https://medium.com/google-developer-experts/running-fetch-in-a-web-worker-700dc33ac854
[28]
Web worker: https://flaviocopes.com/web-workers/
[29]
预加载数据和渐进式 Web 应用程序: https://blog.sessionstack.com/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them-a547c0757f6a
[30]
Comlink: https://github.com/GoogleChromeLabs/comlink
[31]
如何脱离浏览器的主线程运行 JavaScript 的出色指南: https://web.dev/off-main-thread/
[32]
Workerize: https://github.com/developit/workerize
[33]
workerize-loader: https://github.com/developit/workerize-loader
[34]
worker-plugin: https://github.com/GoogleChromeLabs/worker-plugin
[35]
将繁重的计算任务抽离: https://developers.google.com/web/updates/2019/02/hotpath-with-wasm
[36]
WebAssembly: https://webassembly.org/
[37]
浏览器的支持非常出色: https://caniuse.com/#feat=wasm
[38]
JavaScript 和 WASM 之间的函数调用变得越来越快: https://hacks.mozilla.org/2018/10/calls-between-javascript-and-webassembly-are-finally-fast-?/
[39]
甚至在 Fastly 的边缘云中也受支持: https://www.fastly.com/blog/announcing-lucet-fastly-native-webassembly-compiler-runtime
[40]
一系列详细的文章,: https://hacks.mozilla.org/2017/02/a-cartoon-intro-to-webassembly/
[41]
概述: https://blog.logrocket.com/webassembly-how-and-why-559b7f96cd71
[42]
WebAssembly 的日益重要的作用: https://www.youtube.com/watch?v=Z6ZhIA8i_8g
[43]
WebAssembly 简介: https://codelabs.developers.google.com/codelabs/web-assembly-intro/index.html
[44]
解释了 WebAssembly 及其工作方式: https://www.youtube.com/watch?v=6v4E6oksar0
[45]
分享了有关 WebAssembly 的实际案例研究: https://www.youtube.com/watch?v=l2DHjRmgAF8
[46]
WebAssembly 的工作原理以及其有用性: https://blog.logrocket.com/webassembly-how-and-why-559b7f96cd71
[47]
提前编译: https://www.lucidchart.com/techblog/2016/09/26/improving-angular-2-load-times/
[48]
将一些客户端渲染移交到服务端: https://www.smashingmagazine.com/2016/03/server-side-rendering-react-node-express/
[49]
在现代浏览器中得到了很好的支持: http://kangax.github.io/compat-table/es6/
[50]
设置两个版本: https://gist.github.com/newyankeecodeshop/79f3e1348a09583faf62ed55b58d09d9
[51]
支持: https://caniuse.com/#feat=es6-module
[52]
Webpack ESNext Boilerplate: https://github.com/philipwalton/webpack-esnext-boilerplate
[53]
ES 模块的注意实现和陷阱: https://jakearchibald.com/2017/es-modules-in-browsers/
[54]
使用 babel-plugin-lodash: https://github.com/lodash/babel-plugin-lodash
[55]
将一般 requrie 方式改为单个模块引入方式: https://www.contentful.com/blog/2017/10/27/put-your-webpack-bundle-on-a-diet-part-3/
[56]
详细的低维护的智能打包指南: https://www.smashingmagazine.com/2018/10/smart-bundling-legacy-code-browsers/
[57]
ES 模块的注意实现和陷阱: https://jakearchibald.com/2017/es-modules-in-browsers/
[58]
module/nomodule: https://philipwalton.com/articles/deploying-es2015-code-in-production-today/
[59]
差分服务: https://calendar.perfplanet.com/2018/doing-differential-serving-in-2019/
[60]
综合性文章: https://calendar.perfplanet.com/2018/doing-differential-serving-in-2019/
[61]
增量解耦: https://githubengineering.com/removing-jquery-from-github-frontend/
[62]
CSS 和 JavaScript 代码覆盖率工具: https://developers.google.com/web/updates/2017/04/devtools-release-notes#coverage
[63]
找出那些模块并使用 import() 延迟加载: https://twitter.com/TheLarkInn/status/1012429019063578624
[64]
Puppeteer: https://github.com/GoogleChrome/puppeteer
[65]
编程方式收集代码覆盖率,: https://twitter.com/matijagrcic/statuses/1060863620568043520
[66]
导出代码覆盖率结果: https://twitter.com/tkadlec/status/1073330247758684163
[67]
现代和旧式浏览器的代码覆盖率: https://twitter.com/AndyDavies/status/1073339071106297856
[68]
其他用法: https://github.com/GoogleChromeLabs/puppeteer-examples
[69]
自动视觉对比: https://meowni.ca/posts/2017-puppeteer-tests/
[70]
在每次构建时监视未使用的 CSS: http://blog.cowchimp.com/monitoring-unused-css-by-unleashing-the-devtools-protocol/
[71]
非常全面的概述: https://nitayneeman.com/posts/getting-to-know-puppeteer-using-practical-examples/
[72]
purgecss: https://github.com/FullHuman/purgecss
[73]
UnCSS: https://github.com/giakki/uncss
[74]
Helium: https://github.com/geuis/helium-css
[75]
Harry Roberts 的建议: https://csswizardry.com/2018/01/finding-dead-css/
[76]
监测 DevTools 使用 DevTools : http://blog.cowchimp.com/monitoring-unused-css-by-unleashing-the-devtools-protocol/
[77]
指出的那样: https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4
[78]
webpack-libs-optimizations: https://github.com/GoogleChromeLabs/webpack-libs-optimizations
[79]
原生的 Internationalization API: https://blog.logrocket.com/4-alternatives-to-moment-js-for-internationalizing-dates/
[80]
date-fns: https://github.com/date-fns/date-fns
[81]
Luxon: https://moment.github.io/luxon/
[82]
表明: https://www.contentful.com/blog/2017/10/27/put-your-webpack-bundle-on-a-diet-part-3/
[83]
Bundlephobia: https://bundlephobia.com/
[84]
size-limit: https://github.com/ai/size-limit
[85]
将这些成本与 Lighthouse Custom Audit 集成在一起: https://github.com/AymenLoukil/Google-lighthouse-custom-audit
[86]
Vue MDC 适配器: https://speakerdeck.com/addyosmani/web-performance-made-easy?slide=22
[87]
Prepack: https://gist.github.com/gaearon/d85dccba72b809f56a9553972e5c33c4
[88]
Svelte 做到了: https://svelte.technology/
[89]
Rawact Babel 插件: https://github.com/sokra/rawact
[90]
size-limit : https://github.com/ai/size-limit
[91]
他的文章中: https://www.contentful.com/blog/2017/10/27/put-your-webpack-bundle-on-a-diet-part-3/
[92]
Guess.js: https://github.com/guess-js/guess
[93]
Next.js 应用程序: https://github.com/mgechev/guess-next
[94]
Angular 和 React 中: https://blog.mgechev.com/2018/03/18/machine-learning-data-driven-bundling-webpack-javascript-markov-chain-angular-react/
[95]
Webpack 插件: https://github.com/guess-js/guess/tree/master/packages/guess-webpack
[96]
DNStradamus: https://www.npmjs.com/package/dnstradamus
[97]
Quicklink : https://github.com/GoogleChromeLabs/quicklink
[98]
Instant.page: https://instant.page/