欧阳城的技术博客

一个前端技术博客

HTTP 协议中,缓存验证是提高 Web 性能的重要机制。Last-ModifiedETag 是两种主要的缓存验证器,它们帮助服务器和客户端确定资源是否需要重新传输。本文将深入探讨这两种机制的工作原理、优缺点以及它们的协同使用。

Last-Modified 工作原理

Last-Modified 是一个 HTTP 响应头,由服务器发送,表示资源在服务器上最后被修改的时间。

工作流程

第一次请求

  1. 客户端请求一个资源
  2. 服务器响应,并在响应头中包含 Last-Modified: <最后修改时间戳>
  3. 浏览器将资源和 Last-Modified 时间戳一同缓存

再次请求

  1. 客户端再次请求该资源时,会在请求头中携带 If-Modified-Since 字段,其值为之前缓存的 Last-Modified 时间戳
  2. 服务器接收到请求后,会比较这个时间戳和资源当前的最后修改时间
  3. 如果两者相同:说明资源未被修改,服务器返回 304 Not Modified 状态码,不含响应体,浏览器直接从缓存中加载资源
  4. 如果时间戳不同:说明资源已被修改,服务器返回 200 OK 状态码,并发送新的资源内容

ETag 工作原理

ETag(Entity Tag) 是一个 HTTP 响应头,由服务器发送,是一个表示资源特定版本的唯一标识符。这个标识符通常是根据资源内容的哈希值生成的。

工作流程

第一次请求

  1. 客户端请求一个资源
  2. 服务器响应,并在响应头中包含 ETag: "<唯一标识符>"
  3. 浏览器将资源和 ETag 标识符一同缓存

再次请求

  1. 客户端再次请求该资源时,会在请求头中携带 If-None-Match 字段,其值为之前缓存的 ETag 标识符
  2. 服务器接收到请求后,会比较这个标识符和资源当前版本生成的标识符
  3. 如果两者相同:说明资源未被修改,服务器返回 304 Not Modified 状态码
  4. 如果标识符不同:说明资源已被修改,服务器返回 200 OK 状态码,并发送新的资源内容

ETag 相比 Last-Modified 的优势

ETag 可以解决 Last-Modified 的几个重要局限性:

1. 时间精度问题

  • Last-Modified 只能精确到秒:如果资源在短时间内(例如一秒内)被修改了多次,Last-Modified 无法识别,会错误地认为资源没有变化
  • ETag 基于内容:无论修改频率如何,只要内容发生变化就能准确识别

2. 时间戳变化但内容不变

  • Last-Modified 的问题:有时资源内容虽然没有变化,但因为某些原因(如重新部署、服务器处理)导致时间戳被修改,Last-Modified 就会失效,强制客户端重新下载资源
  • ETag 的解决方案:由于基于内容生成,内容不变则标识符不变

3. 分布式系统同步问题

  • Last-Modified 的挑战:在负载均衡的服务器集群中,一台服务器上资源的修改时间可能与另一台服务器不同步,导致缓存验证失败
  • ETag 的优势:基于内容的哈希值在所有服务器上都是一致的

Last-Modified 和 ETag 的协同使用

虽然 ETag 更精确,但它需要服务器额外计算资源的哈希值,这会带来一定的性能开销。因此,服务器通常会同时发送这两个验证器。

验证优先级机制

  1. 当客户端再次请求资源时,它会同时发送 If-Modified-SinceIf-None-Match

  2. 服务器优先使用 ETag 进行验证:这是因为 ETag 提供了更强的验证,如果 ETag 匹配成功,服务器会直接返回 304

  3. Last-Modified 作为备用方案:只有在以下两种情况下,服务器才会退回到 Last-Modified 验证:

    • 客户端只发送了 If-Modified-Since 字段(旧版浏览器或特殊情况)
    • ETag 验证失败,但服务器想使用 Last-Modified 作为备用方案

兼容性与健壮性

在大多数现代浏览器和服务器的实现中,If-None-Match(ETag) 字段的优先级高于 If-Modified-Since(Last-Modified)。这种机制确保了在保证验证精确性的同时,提供一个兼容性更强的后备方案,使得缓存策略更加健壮。

总结

Last-ModifiedETag 都是重要的 HTTP 缓存验证机制:

  • Last-Modified:简单高效,但存在时间精度和同步问题
  • ETag:更精确可靠,但需要额外的计算开销
  • 协同使用:结合两者优势,提供既精确又兼容的缓存验证方案

合理使用这些缓存验证机制,能够显著提高 Web 应用的性能,减少不必要的网络传输,提升用户体验。

大家有时候会听到这么一句话:浏览器是单线程的。这是一个关于浏览器架构的常见误解。

浏览器本身是多进程的

现代浏览器(如 ChromeFirefoxEdge)是多进程架构。每个进程都有其独立的内存空间,互不干扰,这大大增强了浏览器的稳定性。

Chrome 为例,它通常会有以下几个主要进程:

主要进程类型

  • 浏览器主进程 (Browser Process):负责管理浏览器界面(地址栏、书签等)、用户输入、文件存储等。

  • 渲染进程 (Renderer Process):这是最关键的部分,它负责将 HTMLCSSJavaScript 转换为我们看到的网页。通常,浏览器会为每个标签页分配一个独立的渲染进程

  • 插件进程 (Plugin Process):负责运行像 Flash 这样的插件。

  • GPU 进程 (GPU Process):负责处理图形渲染,以提高页面的绘制性能。

多进程架构的优势

这种多进程架构的好处是,如果一个标签页崩溃了,它只会影响该标签页的渲染进程,而不会导致整个浏览器都崩溃。

浏览器的一个页面是多线程的

虽然每个渲染进程是独立的,但它内部并不是单线程的。一个渲染进程内部包含多个线程,例如:

渲染进程内的主要线程

  • 主线程 (Main Thread):这是我们通常所说的”JavaScript 引擎所在的线程”。它负责执行 JavaScript 代码、处理用户事件(如点击、滚动)、执行 DOM 操作和大部分的页面渲染工作。这个线程是单线程的,这意味着它一次只能做一件事。

  • 排版引擎线程 (Layout Thread):负责计算页面元素的布局和位置。

  • 渲染引擎线程 (Painting Thread):负责将元素绘制到屏幕上。

  • 合成线程 (Compositor Thread):负责将不同的图层合成在一起,并将其发送给 GPU 进程。

  • Web Worker 线程:这些是独立的后台线程,用于执行耗时的计算任务,但它们无法直接访问 DOM

架构层次图解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─────────────────────────────────────────────────────────┐
│ 浏览器 (多进程) │
├─────────────────────────────────────────────────────────┤
│ 浏览器主进程 │ 渲染进程 1 │ 渲染进程 2 │ GPU 进程 │
│ (Browser) │ (Tab 1) │ (Tab 2) │ │
│ │ │ │ │
│ • 界面管理 │ ┌──────────┐ │ ┌──────────┐ │ • 图形 │
│ • 用户输入 │ │ 主线程 │ │ │ 主线程 │ │ 渲染 │
│ • 文件存储 │ │(单线程) │ │ │(单线程) │ │ │
│ │ │ │ │ │ │ │ │
│ │ │• JS 引擎 │ │ │• JS 引擎 │ │ │
│ │ │• DOM 操作 │ │ │• DOM 操作 │ │ │
│ │ │• 事件处理 │ │ │• 事件处理 │ │ │
│ │ └──────────┘ │ └──────────┘ │ │
│ │ │ │ │
│ │ • 排版线程 │ • 排版线程 │ │
│ │ • 渲染线程 │ • 渲染线程 │ │
│ │ • 合成线程 │ • 合成线程 │ │
│ │ • Worker 线程 │ • Worker 线程 │ │
└─────────────────────────────────────────────────────────┘

总结

所以,正确的说法是:

🔍 准确的描述

  • 浏览器是多进程的
  • 浏览器的一个渲染进程是多线程的
  • 一个渲染进程中的”JavaScript 引擎”是单线程的

💡 理解要点

当人们说”JavaScript 是单线程的”时,他们通常指的是在浏览器的主线程中,JavaScript 代码是顺序执行的,一次只能处理一个任务,这也解释了为什么需要事件循环等机制来处理异步操作。

🎯 关键概念

  • 多进程:提供稳定性,进程间相互隔离
  • 多线程:提供并发能力,不同线程处理不同任务
  • 单线程JavaScript 执行的特性,确保代码执行的确定性
0%