技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTBR

浏览器灵魂之问,请问你能接得住几个

所属馆藏
Browser
文件格式
Markdown
原始路径
Browser/32-浏览器灵魂之问,请问你能接得住几个
本文目录13 个章节
  1. 第 1 篇:能不能说一说浏览器缓存?
  2. 第 2 篇:浏览器本地存储各自优劣如何?
  3. 第 3 篇:从输入 URL 到页面呈现——网络篇
  4. 第 4 篇:解析算法篇
  5. 第 5 篇:渲染过程篇
  6. 第 6 篇:谈谈重绘和回流
  7. 第 7 篇:能不能说一说 XSS 攻击?
  8. 第 8 篇:能不能说一说 CSRF 攻击?
  9. 第 9 篇:HTTPS 为什么让数据传输更安全?
  10. 第 10 篇:能不能实现事件的防抖和节流?
  11. 第 11 篇:能不能实现图片懒加载?
  12. 总结
  13. 参考资料

浏览器灵魂之问,请问你能接得住几个

Category(分类): Browser, HTML Status: 已更新

浏览器原理是性能、安全和工程实践的基础。本文保留原文的面试题结构:缓存、本地存储、输入 URL、解析、渲染、回流重绘、XSS、CSRF、HTTPS、防抖节流和图片懒加载,同时把其中已经过时或容易误导的结论改成更准确的现代说法。

浏览器实现会随着版本、平台和设备变化,面试时应先讲清通用模型,再说明“实际由浏览器和环境决定”,不要背诵固定连接数、固定缓存位置或固定渲染步骤。

第 1 篇:能不能说一说浏览器缓存?

缓存的目标是减少网络传输、降低延迟和服务器压力。浏览器缓存并不是只有“强缓存”和“协商缓存”两层,还可能有 Service Worker Cache、内存缓存、磁盘缓存、CDN、代理缓存和 BFCache 等不同机制。

强缓存

响应可以通过 Cache-ControlExpires 告诉缓存层,在一段时间内是否可以直接复用。最常见的现代写法是:

Cache-Control: max-age=3600

它表示响应在共享的缓存寿命计算中可以新鲜使用 3600 秒。更完整的指令包括:

  • public:允许共享缓存保存,仍受响应内容、认证和缓存键等规则约束;
  • private:只允许私有缓存保存,常用于个性化响应;
  • no-cache:可以保存,但使用前必须向服务器验证,不等于不保存;
  • no-store:要求缓存不要存储该响应,适合高度敏感或不应落盘的响应;
  • s-maxage:为共享缓存指定新鲜时间,优先于 max-age
  • must-revalidate:过期后不能在无法验证时随意复用;
  • immutable:提示内容在新鲜期内不会变化,常用于带内容指纹的静态资源。

Expires 是 HTTP/1.0 时代的绝对时间字段。它受时钟偏差影响,现代响应通常用 Cache-Control,但为了兼容旧客户端仍可能同时发送:

Cache-Control: public, max-age=31536000, immutable
Expires: Wed, 22 Nov 2034 08:41:00 GMT

当两者同时存在时,符合现代 HTTP 缓存规则的实现会优先使用 Cache-Controlno-cache 不等于 no-store,这是旧文章里最容易混淆的地方。

协商缓存

缓存过期或响应要求重新验证时,浏览器可以发送验证请求,而不一定重新下载完整响应体:

If-None-Match: "article-v8"
If-Modified-Since: Wed, 22 Nov 2023 08:41:00 GMT

服务器可以返回:

HTTP/1.1 304 Not Modified
ETag: "article-v8"

304 表示缓存验证成功,响应通常没有新的消息体,浏览器复用本地的响应内容。若资源已经改变,服务器返回新的响应和新的 ETag

ETag 通常能比秒级 Last-Modified 更准确地识别内容变化,但不是一定要用哈希生成,也不是一定比时间字段慢。服务器可以使用文件版本、内容摘要或其他稳定标识,具体成本取决于实现。若同时提供两者,服务器应按 HTTP 规则处理条件请求,不能只凭经验断言所有场景永远优先某一个字段。

缓存位置与 Service Worker

旧资料常把浏览器缓存位置列成:

  1. Service Worker Cache;
  2. Memory Cache;
  3. Disk Cache;
  4. HTTP/2 Push Cache。

这不是标准规定的固定优先级。浏览器会根据资源类型、生命周期、缓存键、导航方式、内存压力和实现策略选择缓存。Service Worker 也不是“比所有缓存都高一级”的通用缓存层,而是可以拦截受控页面的请求并自行决定返回网络、Cache API 或其他结果。

self.addEventListener('fetch', event => {
  const request = event.request
  if (request.method !== 'GET') return

  event.respondWith(
    caches.match(request).then(cached => {
      if (cached) return cached
      return fetch(request).then(response => {
        const copy = response.clone()
        event.waitUntil(
          caches.open('assets-v1').then(cache => cache.put(request, copy))
        )
        return response
      })
    })
  )
})

Service Worker 有安装、激活和终止生命周期,不是永远运行的后台进程。缓存策略要考虑版本、更新、失败回退、离线数据一致性和敏感响应,不能把所有请求都无条件写入 Cache API。

HTTP/2 Server Push 已从主流浏览器实现中移除,不能再把它当作今天浏览器的“第四级缓存”。现代性能优化更常使用预加载、103 Early Hints、HTTP/2/3 多路复用、缓存指纹和 CDN。

内容指纹和缓存失效

不可变的构建产物适合使用哈希文件名:

app.4f91c2.js
app.4f91c2.css
Cache-Control: public, max-age=31536000, immutable

HTML、配置和 API 响应通常使用较短的新鲜时间或协商缓存。不要给内容可变且无法改名的 URL 设置一年缓存后再期待服务器能及时“回收”旧文件。

第 2 篇:浏览器本地存储各自优劣如何?

常见客户端存储包括 Cookie、localStoragesessionStorage、IndexedDB、Cache API 和内存状态。它们的持久性、同步方式、网络行为、容量和安全边界不同。

Cookie 最初用于弥补 HTTP 无状态带来的会话管理问题。符合 Domain、Path、Secure、SameSite 等条件时,浏览器会自动把 Cookie 附加到请求。

Cookie 的限制:

  1. 单个 Cookie 大小有限,工程上通常按约 4KB 量级规划,具体限制由浏览器实现决定;
  2. 自动随请求发送,会增加请求体积;
  3. HttpOnly Cookie 不能被页面 JavaScript 读取,但仍会自动发送;
  4. 没有 Secure 时可能在不安全连接中暴露;
  5. Cookie 不是通用数据库,不适合保存大量业务数据。

会话 Cookie 常见安全属性:

Set-Cookie: __Host-session=opaque-id; Path=/; Secure; HttpOnly; SameSite=Lax

HttpOnly 可以降低脚本直接读取会话 Cookie 的风险,但不能阻止 XSS 以用户身份发起请求;仍需输出编码、CSP、CSRF 防护和服务端授权。

localStorage

localStorage 按 origin 隔离,接口同步,默认不随 HTTP 请求发送:

const user = {
  name: 'sanyuan',
  age: 18
}

localStorage.setItem('profile', JSON.stringify(user))

const raw = localStorage.getItem('profile')
const profile = raw ? JSON.parse(raw) : null
console.log(profile?.name)

优点是简单、持久、适合少量非敏感偏好;缺点是同步读写可能阻塞主线程,容量受浏览器、设备和存储压力影响,不应把“固定 5MB”当作规范。任何能执行页面 JavaScript 的 XSS 都可能读取其中的令牌,因此不建议把高敏感 Access Token 放进去。

sessionStorage

sessionStorage 也按 origin 隔离,但通常绑定到当前顶级浏览上下文的会话。刷新页面一般仍在同一会话内,关闭标签页或窗口后通常被清理;新窗口是否复制初始值还与 opener 和浏览器行为有关。

它适合保存当前页面流程的临时草稿、一次性表单状态和返回位置,不适合当作可靠的跨标签页消息系统。

IndexedDB 和 Cache API

IndexedDB 是异步的事务型客户端数据库,支持对象仓库、索引、结构化克隆和二进制数据。它不是“理论上无限容量”:实际配额、持久化资格、用户清理和浏览器策略都会限制可用空间。

const request = indexedDB.open('notes', 1)

request.onupgradeneeded = () => {
  request.result.createObjectStore('items', { keyPath: 'id' })
}

request.onsuccess = () => {
  const db = request.result
  const tx = db.transaction('items', 'readwrite')
  tx.objectStore('items').put({
    id: crypto.randomUUID(),
    text: 'hello'
  })
}

Cache API 更适合保存 Request/Response 对,常和 Service Worker 配合离线资源。它和 IndexedDB、HTTP 缓存不是同一种东西,也不会自动替代应用层的数据一致性设计。

第 3 篇:从输入 URL 到页面呈现——网络篇

1. URL 和缓存

浏览器先规范化 URL,再检查 Service Worker、HTTP 缓存和已有页面状态。命中强缓存时可能不发网络请求;缓存过期后可能发送带 If-None-MatchIf-Modified-Since 的验证请求。

2. DNS 解析

DNS 负责把主机名映射到一个或多个 IP 地址。浏览器、操作系统和递归解析器都可能缓存记录,因此 DNS 不只在“第一次访问”发生。CDN、IPv6、代理、DoH/DoT 和网络切换都会改变真实链路。

输入 URL 到网络响应的概览

3. 建立连接

HTTP/1.1 和 HTTP/2 over TCP 通常先进行三次握手,再进行 TLS(HTTPS)协商。连接可以复用,HTTP/2 可以多路复用多个流。

HTTP/3 使用 QUIC,不经过 TCP 三次握手;QUIC 运行在 UDP 之上,并集成 TLS 1.3、可靠流和拥塞控制。因而“输入 URL 后一定是 DNS → TCP 三次握手 → TLS → HTTP”只是 TCP 版本的简化路径。

4. 发送 HTTP 请求

请求的概念结构如下:

GET / HTTP/1.1
Host: www.example.com
Accept: text/html,application/xhtml+xml
Accept-Encoding: gzip, br
Cache-Control: no-cache
Cookie: session=<省略>
User-Agent: <browser>

HTTP/2 和 HTTP/3 在线路上使用二进制帧,不能把上面的 HTTP/1.1 文本直接当作 HTTP/2 的线路格式。请求体也不只由 POST 使用,是否有请求体由方法和应用语义决定。

服务器响应包括状态、头字段和消息体:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Encoding: br
Cache-Control: no-cache

200 不是唯一的正常状态:204206304、重定向等也可能是符合预期的响应。Connection: keep-alive 是 HTTP/1.x 的连接提示;HTTP/2 和 HTTP/3 使用持久连接和多路复用,不应在请求中发送连接级 Connection 头。

第 4 篇:解析算法篇

如果响应的 MIME 类型和导航上下文允许作为 HTML 处理,浏览器就会启动 HTML tokenizer 和 tree builder。

1. HTML 不是普通 XML

原文用“上下文无关文法”解释 HTML 的特殊性,这个方向有启发性,但不宜得出“HTML 标准本身就是简单的非上下文无关文法”这种结论。HTML 规范定义的是带状态的 tokenizer 和建树算法,包含插入模式、开放元素栈、活动格式化元素和大量错误恢复规则。

HTML 解析算法示意

2. 标记化

输入字符会根据当前状态生成 DOCTYPE、开始标签、结束标签、属性、注释、文本和文件结束等 token:

<html>
  <body>
    Hello browser
  </body>
</html>

遇到 < 时进入标签打开状态,读取标签名和属性,遇到 > 后生成 token 并回到合适状态。真实实现还要处理实体、脚本数据、注释、CDATA 兼容行为和错误输入。

3. 建树和容错

建树器收到 token 后,会:

  1. 创建或寻找对应的 DOM 节点;
  2. 把节点插入 DOM;
  3. 根据插入模式维护开放元素栈等状态。

浏览器会对省略的 head/body、错误嵌套、表格、<form><br> 等执行标准规定的纠错。开发时不要依赖错误 HTML 的具体纠错结果,应写符合规范的标记。

4. CSS 和布局树

CSS 解析后,浏览器根据来源、层叠、继承、选择器、媒体查询和自定义属性计算样式,再创建布局盒或 fragment。display: none 通常不参与布局,visibility: hidden 通常仍占空间。

早期 Chrome 资料使用“Render Tree”描述 DOM 和样式的结合。今天仍可用它解释可见内容,但 Chromium 内部实际使用布局树、属性树、绘制块和合成结构,不能把 Render Tree 当作所有浏览器都公开的固定对象。

第 5 篇:渲染过程篇

布局之后,浏览器要把几何和样式转成屏幕像素。可以拆成:

  1. 建立合成相关结构;
  2. 生成绘制列表;
  3. 把绘制内容划分为图块并栅格化;
  4. 合成并提交给显示系统。

渲染流水线示意

1. 图层树和合成

层叠上下文负责层叠顺序,不等于独立合成层。transformopacity 动画、视频、canvas、滚动容器、裁剪和 will-change 等可能促使浏览器创建独立合成资源,但具体条件是启发式的。

2. 绘制列表、图块和栅格化

Paint 记录背景、文本、边框、阴影等绘制指令;Raster 执行指令生成图块位图;Composite 根据变换、透明度、裁剪和滚动状态合成。图块大小不是固定的 256 或 512 像素,栅格化也不一定全部使用 GPU。

图块和合成示意

“只要使用 translateZ(0) 就能开启硬件加速”是过时军规。过多图层会消耗内存、增加栅格化和上传成本,应该用 Performance 和 Layers/Rendering 面板验证。

第 6 篇:谈谈重绘和回流

1. 回流/布局

改变几何关系可能需要 Layout,例如改变宽高、边距、定位、字体、内容或增删可见节点。布局影响范围可以是局部,也可以扩展到父级和后续内容。

读取 offsetWidthgetBoundingClientRect() 等布局信息时,如果之前有尚未提交的样式写入,浏览器可能执行强制同步布局。getComputedStyle() 是否进一步触发布局取决于读取的属性和文档状态,不能说每次调用都必然回流。

2. 重绘

只改变颜色、背景、边框、阴影等外观时,通常可以跳过布局,但可能需要 Paint 和 Raster。重绘区域很大或内容很复杂时,成本仍然可能很高。

布局、绘制与合成的关系

常见原则仍然成立:布局可能带来后续绘制,绘制不一定需要重新布局。但具体实现可能通过缓存、增量布局、失效区域和合成优化工作。

3. 减少布局抖动

const cards = [...document.querySelectorAll('.card')]

// 先读
const heights = cards.map(card => card.getBoundingClientRect().height)

// 后写
cards.forEach((card, index) => {
  card.style.setProperty('--measured-height', `${heights[index]}px`)
})

不要把下面这种读写交错写在高频循环里:

for (const card of cards) {
  card.style.width = `${card.offsetWidth + 1}px`
}

可以批量修改 class、使用脱离文档的 DocumentFragment、使用 contain/content-visibility,但这些方案都要考虑可访问性、尺寸占位、焦点和真实测量。

第 7 篇:能不能说一说 XSS 攻击?

XSS(Cross-Site Scripting)是攻击者控制的数据被浏览器当作脚本或危险 HTML 执行。它并不要求“跨域脚本”,恶意代码通常是在受害站点的上下文中执行,从而可能读取页面数据、操作 DOM、发起同源请求或窃取非 HttpOnly Cookie。

常见类型:

  • 存储型 XSS:恶意内容被保存到数据库、评论或用户资料,之后被多个用户加载;
  • 反射型 XSS:恶意输入经 URL 或请求参数进入响应;
  • DOM 型 XSS:危险数据在浏览器端经过 innerHTMLeval、危险 URL 等路径进入执行上下文,不一定经过服务器。
const output = document.querySelector('#output')
const value = new URLSearchParams(location.search).get('q') ?? ''

// 安全的纯文本展示
if (output) output.textContent = value

防护应按输出上下文编码,而不是简单关键词过滤:

  • HTML 文本使用安全模板或文本节点;
  • HTML 属性、URL、JavaScript、CSS 上下文分别编码和校验;
  • 确实需要富文本时使用成熟 sanitizer,并限制允许的标签和协议;
  • 设置 CSP,优先使用 nonce/hash,逐步收紧 script-src
  • 对大型应用评估 Trusted Types;
  • Cookie 使用 HttpOnlySecureSameSite,但记住 HttpOnly 不能阻止 XSS 以当前用户身份发请求;
  • 服务端仍需做鉴权、输入校验和审计。

CSP 是纵深防御,不是修复输出编码漏洞的替代方案。

第 8 篇:能不能说一说 CSRF 攻击?

CSRF(Cross-Site Request Forgery)利用浏览器自动携带的 Cookie 或其他凭证,让受害者在已登录状态下向目标站点发起非预期请求。它主要威胁会自动附加凭证的认证方式。

1. 不要让有副作用的操作使用 GET

<img src="https://bank.example/transfer?to=attacker&amount=100">

即使现代浏览器的 SameSite 默认策略降低了部分跨站 Cookie 携带,也不能把 GET 设计成转账、删除等有副作用操作。GET 应尽量保持安全和幂等。

2. 跨站表单

<form action="https://shop.example/account/email" method="POST">
  <input type="hidden" name="email" value="attacker@example.net">
</form>
<script>
  document.forms[0].submit()
</script>

如果目标站点只依赖自动携带的 Cookie,且没有检查请求意图,服务器可能误把请求当作用户操作。

3. SameSite

  • Strict:限制最严格,跨站场景几乎不发送 Cookie;
  • Lax:现代浏览器常见默认策略,允许部分顶级安全导航携带 Cookie;
  • None:允许跨站携带,但必须同时设置 Secure

SameSite 是重要防线,但要根据登录跳转、嵌入式应用、跨站支付和浏览器兼容性设计,不能单独当成所有 CSRF 场景的保证。

4. CSRF Token 与来源校验

服务端可以为会话生成不可预测的 CSRF Token,并要求状态改变请求通过隐藏字段或自定义请求头提交。对于 Cookie 会话,还可以检查 Origin,必要时使用 Referer 作为补充:

async function updateProfile(data, csrfToken) {
  const response = await fetch('/api/profile', {
    method: 'POST',
    credentials: 'same-origin',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrfToken
    },
    body: JSON.stringify(data)
  })

  if (!response.ok) throw new Error('更新失败')
}

浏览器脚本不能随意设置受保护的 OriginReferer 请求头,因此服务端来源校验不是“可以被 Ajax 轻易伪造”。当然,服务端仍要考虑代理、隐私策略、缺失 Referer 和非浏览器客户端,并使用 Token、SameSite、授权和重放控制组合防护。XSS 一旦存在,攻击者可能在受信任页面内读取 Token 或直接发请求,所以必须先修复 XSS。

第 9 篇:HTTPS 为什么让数据传输更安全?

HTTP 本身不提供机密性和完整性。HTTPS 把 HTTP 放到 TLS 中,防止网络观察者直接读取或篡改应用数据,并通过证书验证服务端身份。

对称与非对称密码

  • 对称加密使用同一个共享密钥,速度快,适合传输大量数据;
  • 非对称密码使用公私钥,适合身份认证和密钥协商,但直接加密大量数据成本较高;
  • TLS 通常使用非对称签名/临时密钥交换建立共享秘密,再使用对称 AEAD 加密应用数据。

旧文章用“浏览器生成 pre_random,再用服务器公钥加密”描述 RSA 密钥交换。这个模型可以帮助理解混合密码,但不是现代 TLS 1.3 的通用流程。TLS 1.3 通常使用 ECDHE key share,并使用握手签名认证服务器;私钥不是用来加密所有响应正文的。

证书和 CA

证书通常包含域名、主体、有效期、公钥、用途和签发者等信息。CA 使用自己的私钥对证书内容签名,浏览器使用受信任 CA 的公钥验证签名,并检查域名、有效期、吊销策略和用途。

如果攻击者仅仅通过 DNS 劫持把请求导向自己的服务器,伪造的证书通常无法通过浏览器的信任链和域名校验,浏览器会发出证书警告。HTTPS 不能修复服务器自身漏洞、错误授权、XSS 或泄露到 URL/日志中的敏感信息。

HTTPS 加密和证书认证流程

第 10 篇:能不能实现事件的防抖和节流?

节流

节流限制一段时间内最多执行一次,常用于滚动、拖拽和窗口变化。下面的实现支持尾调用:

function throttle(fn, interval, options = {}) {
  let lastTime = 0
  let timer = null
  let lastArgs
  let lastThis

  const invoke = () => {
    lastTime = Date.now()
    timer = null
    fn.apply(lastThis, lastArgs)
    lastArgs = undefined
    lastThis = undefined
  }

  return function throttled(...args) {
    const now = Date.now()
    if (!lastTime && options.leading === false) lastTime = now

    const remaining = interval - (now - lastTime)
    lastArgs = args
    lastThis = this

    if (remaining <= 0 || remaining > interval) {
      if (timer) clearTimeout(timer)
      invoke()
    } else if (!timer && options.trailing !== false) {
      timer = setTimeout(invoke, remaining)
    }
  }
}

防抖

防抖会在连续触发停止一段时间后执行,适合搜索输入、窗口停止调整等场景:

function debounce(fn, delay) {
  let timer = null

  return function debounced(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      timer = null
      fn.apply(this, args)
    }, delay)
  }
}

实际应用还要处理取消、立即执行、组件卸载和异步请求竞态。防抖不等于取消已经发出的网络请求,必要时应配合 AbortController

第 11 篇:能不能实现图片懒加载?

原文使用 clientHeightscrollTopoffsetTop 轮询,这有助于理解原理,但手写滚动监听容易出现边界、节流和布局读取问题。现代浏览器优先支持:

<img
  src="placeholder.webp"
  data-src="target.webp"
  alt="示例图片"
  width="1200"
  height="800"
  loading="lazy"
  decoding="async"
>

loading="lazy" 是浏览器提示,不保证所有资源都以同一距离加载;首屏 LCP 图片通常不应懒加载。

需要自定义预加载距离时使用 IntersectionObserver

const images = document.querySelectorAll('img[data-src]')

const observer = new IntersectionObserver((entries, currentObserver) => {
  for (const entry of entries) {
    if (!entry.isIntersecting) continue

    const image = entry.target
    const source = image.dataset.src
    if (source) image.src = source
    image.removeAttribute('data-src')
    currentObserver.unobserve(image)
  }
}, {
  rootMargin: '200px 0px'
})

images.forEach(image => observer.observe(image))

这里直接使用已经创建的 observer 观察每张图片即可。

图片应提供 widthheight 或合适的 aspect-ratio,避免加载后改变布局;同时应根据网络、图片格式、响应式尺寸和 LCP 实际数据调优。

总结

这组问题真正考察的是建立因果链:

缓存和连接
  -> HTTP 响应
  -> HTML/CSS/JS 解析
  -> 样式、布局、绘制、栅格化、合成
  -> 用户体验和性能

Cookie/凭证
  -> XSS、CSRF、HTTPS、会话安全

高频事件和资源加载
  -> 防抖、节流、懒加载、LCP、INP、CLS

回答原理题时,可以先给出通用模型,再指出协议版本、浏览器实现和设备条件会改变细节,最后用 Network、Performance、Elements 和真实用户监控验证结论。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS