从【字节码缓存】再进一步看【HTTP 缓存】,面试官:“这么细吗?”

2,813 阅读7分钟

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

字节码缓存

什么是字节码缓存?

字节码缓存(Bytecode Cache),是浏览器性能优化机制中重要的一项,通过缓存 解析(pasing)+编译(compilation)的结果,减少网站的启动时间;当前市面上主流的浏览器都实现了字节码缓存功能;

以 Chrome 为例,V8 早期采用了直接生成二进制机器码的方式,即:

js 源代码 => AST 抽象语法树 => 二进制文件

image.png

虽然当时也采用了二进制代码缓存(缓存到内存和硬盘上)来省去重复编译时间;

但是,二进制代码的问题在于:1. 内存占用大;2. 代码复杂度高;3. 惰性编译只缓存最外层代码;

于是乎,后来 V8 也引进了字节码架构:

js 源代码 => AST 抽象语法树 => 字节码 => 二进制文件

image.png

引入字节码的优势在于:1. 内存占用小;2. 编译快、启动快;3. 降低代码复杂度;

除了 Google,同行 Mozilla 也应用了字节码缓存机制; 更多见官网 blog 介绍: 《Mozilla - JavaScript 启动字节码缓存》

image.png

image.png

并且还对比了是否启用 JSBC(The JavaScript Startup Bytecode Cache)在主流网站的性能表现统计:

image.png

两级缓存策略

正片开启 —— 两级缓存策略!

实际上,对于 V8 编译后的字节码,Chrome 有两级缓存策略:

  1. Isolate 内存缓存;
  2. 完整序列化的硬盘缓存;

1. Isolate 内存缓存:

Isolate 缓存发生在同一进程中(同一页面中),它试图尽可能快而小地使用已经可用的数据;

它的劣势在于:a. 命中率偏低(80%);b. 不能跨进程;

当 V8 编译脚本时,编译后的脚本以源码为键被存储在一个 hashtable 中(在 V8 的堆中),当 Chrome 要求 V8 编译其他脚本的时候,V8 首先检查脚本的源码是否能匹配 hashtable 中的值。如果是,则返回已经存在的字节码。

2. 完整序列化的硬盘缓存:

硬盘缓存是由 Chrome 管理(准确来说是由 Blink ),它填充了 Isolate 缓存不能在多个进程或多个 Chrome 会话间共享代码缓存的空白。

它利用现有的 HTTP 资源缓存,管理从 Web 接收的缓存和过期数据,具体过程是:

当首次请求一个 JS 文件(即 cold run)时,Chrome 会下载它并将其交给 V8 进行编译。它还将文件存储在浏览器的磁盘缓存中。

当第二次请求 JS 文件(即 warm run)时,Chrome 会从浏览器缓存中获取该文件,并再次将其提供给 V8 进行编译。然而,这一次编译的代码被序列化,并作为元数据附加到缓存的脚本文件中。

第三次(即 hot run),Chrome 从缓存中获取文件和文件的元数据,并将两者交给 V8。V8 反序列化元数据并且可以跳过编译。

image.png

warm run 时使用内存缓存,在 hot run 时使用磁盘缓存;

再看 HTTP 缓存

知己知彼、百战不殆!

根据对【字节码缓存】以及 【两级缓存策略】的认知,我们将有更清晰的思路来利用浏览器缓存机制提升网站的加载性能!

还记得面试中常问的这张关于强缓存、协商缓存流程图吗?

image.png

从字节码缓存角度,我们可以告诉面试官更多细节!

当服务器返回 304 Not Modified 时,我们的字节码缓存保持着 warm run(暖运行)hot run(热运行);当返回为  200 OK 时,更新缓存资源,并且清除字节码缓存,恢复到 cold run(冷运行)状态;(哇!HTTP 缓存文件再进一步的理解!从【文件缓存】到【文件编译后的字节码缓存】的理解~)

所以,减少代码变更,仍然是利用缓存最基础也是最有效的一条准则;

当然,与之同为一个道理的是:不随意修改资源的 URL,因为字节码缓存与脚本的 URL 关联,修改 URL,会在浏览器的缓存资源中创建一个新的资源入口,并伴随着一个冷缓存入口;

还有 1 个细节就是,我们做 A/B 测试时,也可能出现缓存不同的资源,所以 保证运行的确定性,也是保证能走对应的缓存的前提:

if (Math.random() > 0.5) {
  A();
} else {
  B();
}

还有一个老生常谈的 HTTP 缓存策略:将稳定的第三方库分离成独立文件

通常来说,我们不建议将所有 JS 脚本合并到一个大的 bundle 中,将其分成多个较小脚本往往更有利于除了字节码缓存之外的其他原因(如:多个网络请求、流编译、页面交互等)。

注:字节码缓存的最小文件大小限制是 1 Kb,文件太小也不行;

强制编译

不过,这里仔细想一想:

只有在代码执行完成时编译了的代码才会被加入到字节码缓存中,那么有许多类型的函数尽管会稍后执行,但也不会被缓存。如:事件处理程序(甚至是 onload)、promise 链、未使用的库函数和其他一些延迟编译而没有在执行到 </script> 之前被调用的,都会保持延迟而不会被执行,它们是不会作为字节码被缓存;

怎么办?

噢,我们可以通过 立即执行函数(IIFE)来强制编译它们!

(function foo() {
  // …
})();

因为 IIFE 表达式会被立即调用,大多数 JavaScript 引擎会尝试探测它们并立即编译,然后进行完全编译;

由于探测手段不同,现在,即使函数实际不是立即执行也会被编译,如下:

const foo = function() {
  // Lazily skipped
};
const bar = (function() {
  // Eagerly compiled
});

(秒啊)

service worker 缓存

service worker 中也有字节码缓存机制;

我们知道 service worker 可以让你构建本地资源缓存,当你发送请求的时候,会从本地缓存提供资源。如果你想构建离线应用,这点特别有用,例如:PWA 应用。

代码示例:

  • service worker 为安装(创建资源)和获取(从潜在的缓存提供资源)事件添加处理程序。
// sw.js
self.addEventListener('install', (event) => {
  async function buildCache() {
    const cache = await caches.open(cacheName);
    return cache.addAll([
      '/main.css',
      '/main.mjs',
      '/offline.html',
    ]);
  }
  event.waitUntil(buildCache());
});

self.addEventListener('fetch', (event) => {
  async function cachedFetch(event) {
    const cache = await caches.open(cacheName);
    let response = await cache.match(event.request);
    if (response) return response;
    response = await fetch(event.request);
    cache.put(event.request, response.clone());
    return response;
  }
  event.respondWith(cachedFetch(event));
});

这些缓存包括 JS 资源缓存。但 service worker 的缓存主要用于 PWA,所以它与 Chrome 的“自动”缓存的机制略微不同;

service worker 中,当 JS 资源被添加到缓存的时候,它们立即创建字节码缓存,这意味着在第二次加载的时候字节码缓存是可用的(而不是像前文所讲的两级缓存一样仅在第三次加载的时可用);

其次,service worker 为这些脚本生成了 “全量”字节码缓存,不存在有延迟编译,而是全部编译好放到缓存中。这具有快速且可预测的性能的优点,没有执行顺序依赖性,但是这以增加的内存使用为代价;

请注意,此策略仅适用于 service worker 缓存,而不适用于 Cache API 的其他用途。实际上,现在的 Cache API 不会执行字节码缓存。

阶段小结

以上,本文先讲了什么是字节码,然后讲了字节码的两级缓存策略,再讲了从字节码的缓存机制中看 HTTP 缓存,给到对应的建议及不同角度的理解(核心);再讲了一个细节:强制编译;最后讲了在 service worker 缓存中,字节码缓存的差异;

对于一部分开发人员来讲,缓存啥的,能用就行,性能啥的,loading 就行;但是认真去了解浏览器的内部一些机制后,会发现:我们之所以站的这么高,是因为踩在了巨人的肩膀上!

06b9e718.gif

ok,打完收工!

撰文不易,点赞鼓励 👍👍👍👍👍👍

我是掘金安东尼,公众号同名,输出暴露输入,技术洞见生活,再会~

参考文献