一次请求,要穿过多少层
浏览器这块有个特点:概念都听过,但一追问就会露馅。比如"同源策略限制什么",多数人会答"不能跨域请求"——其实它限制的是读取响应,请求本身早就发出去了。下面每一节都在纠这种半对半错的理解。
01先画流程缓存、CORS、渲染都有明确顺序,把顺序理清,细节自然就挂上去了。
02分清"拦住了什么"安全机制最容易记混:拦的是发送、读取,还是执行?问清楚这一点就不会答错。
03连到性能上缓存、渲染、事件每一节都直接影响性能,能串起来讲比单点回答更值钱。
01
从输入 URL 到页面出现★
一条链路,六个阶段
面试问"输入 URL 之后发生了什么",不要从 DNS 开始背。先给出六个阶段的骨架,再挑一个展开——这比流水账更能体现你真的理解。
解析 URL协议 / 域名 / 端口 / 路径
→
查缓存浏览器 → 系统 → 路由
→
建立连接DNS → TCP → TLS
→
发请求HTTP 报文 + Cookie
→
解析渲染HTML → DOM,CSS → CSSOM
→
加载子资源JS / 图片 / 字体
- 最容易被追问的一步是 DNS:先查浏览器缓存、系统 hosts、本地 DNS 服务器,再递归到根域名服务器。它是有层次的,不是"一次查询"。
- TLS 握手的关键是"几趟":TLS 1.2 要两个 RTT,TLS 1.3 压到一个 RTT,而且支持 0-RTT 恢复。这一趟的差别直接决定首屏快不快。
- 域名和 IP 不是一一对应:一个域名可以有多个 A 记录做负载均衡,CDN 还会按地域返回不同的 IP。
面试怎么答整体链路是:解析 URL → 各级缓存 → DNS 解析 → TCP 三次握手 → TLS 握手 → 发送 HTTP 请求 → 接收响应 → 解析 HTML 构建 DOM 和 CSSOM → 布局绘制 → 加载子资源并执行 JS。面试时给出这个骨架,然后挑 DNS 或 TLS 深入。
02
浏览器不是一个进程★
one tab can crash without taking the browser down
Chrome 是多进程架构,不是"一个程序跑一个页面"。拆开的核心动机只有三个:
一个页面崩了别把整个浏览器带走(稳定性)、把渲染进程关进沙箱(安全)、
让多个页面真正并行(性能)。代价是内存占用高——这也是它被叫"内存杀手"的原因。
五个主要进程,各自负责什么
浏览器主进程:只有一个。管窗口、标签页、地址栏、前进后退、以及所有其他进程的创建和调度。
渲染进程:每个标签页一个(严格说是每个站点实例一个)。负责 HTML 解析、样式计算、布局、绘制、执行 JS。前端代码全跑在这里。
GPU 进程:只有一个。负责合成层、3D 变换、硬件加速。
网络进程:负责所有网络请求。以前在浏览器进程里,现在独立出来。
插件进程:每个插件一个,用完就退。
渲染进程:每个标签页一个(严格说是每个站点实例一个)。负责 HTML 解析、样式计算、布局、绘制、执行 JS。前端代码全跑在这里。
GPU 进程:只有一个。负责合成层、3D 变换、硬件加速。
网络进程:负责所有网络请求。以前在浏览器进程里,现在独立出来。
插件进程:每个插件一个,用完就退。
- 为什么渲染进程能崩而浏览器不崩:它是独立进程,内存空间隔离。渲染进程 OOM 被系统杀掉,主进程收到通知后给这个 tab 显示"页面崩溃",其他 tab 毫发无损。如果是单进程架构,一个页面死循环就能让整个浏览器假死。
- 沙箱是安全的地基:渲染进程跑在受限环境里,默认不能直接读文件、不能访问系统调用,所有需要特权的操作都要通过 IPC 请求主进程代办。这样即使页面被 XSS 拿到了代码执行权,也攻不破浏览器本身。
- 渲染进程内部还是多线程:主线程(解析、样式、布局、JS 全在这——所以 JS 阻塞渲染)、合成线程(滚动和合成动画,能脱离主线程独立跑)、光栅线程(把图层转成位图)、以及若干 Worker 线程。"JS 是单线程"说的是渲染主线程。
- 一个 tab 一个进程并不严格:Chrome 会做进程复用。同一个站点(site)的多个标签页,可能共享一个渲染进程,为的是省内存;不同站点则强制隔离,这是 Site Isolation 策略,防的是 Spectre 那类侧信道攻击。
- "打开一个空白页也要几百 MB":每个渲染进程都要带一份 V8 引擎、一份 Blink 渲染引擎的基础开销。所以进程数不是越多越好,浏览器会按内存压力动态合并。
- iframe 默认和父页面同进程:如果 iframe 跨站,Site Isolation 会把它放进独立进程,这时候父子页面之间的直接 DOM 访问会被阻止,只能靠 postMessage 通信。
✕ 常见误解
- "一个标签页就是一个进程" —— 同站点的 tab 可能合并
- "多进程是为了快" —— 主要是为了稳定和安全,性能是副产品
- "JS 单线程所以浏览器是单线程" —— 浏览器内部线程非常多
最后一条最容易答错:说"JS 是单线程"指的是每个渲染主线程只有一个 JS 执行栈,不是整个浏览器只有一个线程。
✓ 面试可以补的一句
- 进程间靠 IPC 通信,所以跨进程的 DOM 操作代价很高
- 合成线程能独立处理滚动,所以
transform动画不掉帧 - Web Worker 是渲染进程内的线程,共享进程内存但不共享执行栈
把这三点说出来,说明你理解的是"线程模型",而不是背了个"多进程"的名词。
面试这样答
Chrome 采用多进程架构,主要有五类进程:浏览器主进程负责窗口和标签页管理、渲染进程负责解析 HTML、计算样式、布局绘制和执行 JS、GPU 进程负责合成和硬件加速、网络进程负责请求、插件进程。拆成多进程主要是为了三点:一是稳定性,单个页面崩溃或 OOM 不会影响其他标签页;二是安全,渲染进程跑在沙箱里,不能直接访问系统资源,需要特权的操作都要通过 IPC 请求主进程;三是性能,多个页面可以真正并行。渲染进程内部还是多线程的,主线程负责解析、布局和 JS 执行,合成线程负责滚动和合成动画,所以 transform 动画能脱离主线程独立跑而不掉帧。JS 所谓的单线程,指的是渲染主线程只有一个 JS 执行栈,不是整个浏览器单线程。
03
HTTP 缓存:强缓存与协商缓存★
为什么改了代码要带 hash
强缓存命中时根本不发请求,直接读本地;协商缓存要发一次请求,服务端说"没变"就回 304、不传内容。前者快但难更新,后者能及时更新但多一次往返。
实际策略几乎都长这样
index.html Cache-Control: no-cache 每次都协商,保证拿到新入口
main.[hash].js Cache-Control: max-age=31536000, immutable 一年,永不失效
文件名带 hash → 内容变则文件名变 → 天然不需要"清缓存"
- no-cache 和 no-store 不是一回事:
no-cache是"可以存,但用之前必须问服务端";no-store是"彻底别存"。搞混会导致敏感信息落到磁盘里。 - ETag 比 Last-Modified 准:后者精确到秒,一秒内改两次就判断错了;前者是内容指纹。代价是服务端要算哈希。
- 304 依然有开销:一个往返的延迟省不掉,所以入口页面之外的东西都应该用强缓存 + hash。
- 刷新行为会变:F5 会让浏览器带上
max-age=0强制协商;Ctrl+F5 连协商都跳过。
面试怎么答强缓存靠 Cache-Control 的 max-age 和 immutable,命中时不发请求;协商缓存靠 ETag/Last-Modified,服务端比较后返回 304。工程上的标准做法是 HTML 用 no-cache 保证入口可更新,静态资源用内容 hash 加一年的 max-age。
04
HTTP/1.1 → 2 → 3:三个层次的队头阻塞★
每次升级都在解同一个问题
队头阻塞其实有三层:应用层(同域并发限制)、HTTP 层(响应必须按序)、TCP 层(丢包会阻塞所有流)。1.1 卡在第一层,2 解决了前两层,3 换 UDP 才彻底解决第三层。
HTTP/1.1每域 6 个并发,排队等
→
HTTP/2一个 TCP 连接多路复用
→
HTTP/3QUIC over UDP,流之间互不影响
- 1.1 的老问题:每个请求一个连接(keep-alive 能复用但有并发上限),而且头部是纯文本、每次重复发送。域名分片就是为了绕过 6 个并发限制的补丁。
- 2 的真问题:所有流共用一个 TCP 连接,只要有一个包丢了,整条连接上所有流都要等重传。这就是 TCP 层的队头阻塞,HTTP/2 自己解决不了。
- 3 的代价:QUIC 在 UDP 上自己实现可靠传输,NAT 和防火墙对 UDP 不友好,所以很多场景仍会回退到 2。运营商限速也是原因之一。
HTTP/2 带来的
- 头部压缩(HPACK),省掉重复的 Cookie 和 UA
- 多路复用,一个连接并发跑所有请求
- 服务端推送(实际已被废弃)
HTTP/2 没解决的
- 单个流的阻塞仍会拖累整条连接
- 每丢一个包,所有流一起等
- 所以弱网下 2 有时比 1.1 还慢
面试怎么答队头阻塞分三层:HTTP/1.1 受同域并发数限制;HTTP/2 用多路复用解决了应用层和 HTTP 层的队头,但底层共用一个 TCP 连接,丢包时所有流都被阻塞;HTTP/3 改用 QUIC over UDP,每个流独立重传,才彻底解决。
05
同源策略与 CORS★
同源策略限制的是"读",不是"发"
同源策略不阻止请求发出,它阻止的是读取响应。所以 POST 请求其实已经到达服务端并执行了——这也是 CSRF 能成立的原因。
什么情况不触发预检(简单请求)
满足这三个条件才是简单请求,直接发送、不预检:
方法 ∈ { GET, HEAD, POST }
头都是安全列表内的(Accept / Accept-Language / Content-Language
/ Content-Type)
Content-Type ∈ { text/plain, multipart/form-data,
application/x-www-form-urlencoded }
application/json 不在名单里 → 一定预检
- 同源的三要素:协议、域名、端口,任一不同就是跨域。子域名也算。
- 带 Cookie 时 ACAO 不能写 *:必须回具体的源,并且加
Access-Control-Allow-Credentials: true,前端还要设credentials: 'include'。 - 最简单的方案是同源代理:开发环境用 devServer proxy,生产环境用 Nginx 反代到同一域名下,从根上避免跨域。
- 预检结果会被缓存:
Access-Control-Max-Age决定缓存多久,设小了会让每个请求多一次 OPTIONS。
面试怎么答同源策略限制的是跨源读取响应和访问 DOM,不限制请求发送。CORS 通过响应头授权,非简单请求会先发 OPTIONS 预检。带凭证时 Allow-Origin 不能是通配符,必须回具体源并允许 credentials。
06
Cookie、Storage、IndexedDB 怎么选★
容量、生命周期、是否自动携带
三者的分界线很清楚:Cookie 会被每次请求自动带上(所以只能存少量身份信息),localStorage 同步阻塞(别存大对象),IndexedDB 异步大容量(适合离线数据)。
localStorage 的代价
- 同步 API,读写会阻塞主线程
- 只有字符串,存对象要 JSON 序列化
- 同源共享,标签页之间会互相覆盖
- 5MB 左右上限,超了直接抛异常
IndexedDB 适合的
- 结构化数据、二进制 Blob
- 异步,不阻塞渲染
- 支持索引和事务查询
- 容量按磁盘配额,通常几百 MB
- 容量与共享范围:Cookie 约 4KB 且每次请求都带;localStorage 和 sessionStorage 约 5MB,前者跨标签页共享、后者只在当前标签页(刷新保留、新开标签不共享)。
- 安全属性比容量更重要:
HttpOnly让 JS 读不到(防 XSS 偷 token),Secure只在 HTTPS 下发送,SameSite限制跨站携带。 - localStorage 不是"永久":它是按源隔离的,用户清缓存就没了;浏览器在存储压力下也可能回收。
面试怎么答Cookie 4KB、每次请求自动携带,适合存会话标识,配合 HttpOnly 和 Secure 使用。localStorage 约 5MB、同步、跨标签共享,适合存轻量偏好。sessionStorage 只在当前标签页。IndexedDB 异步、容量大、支持索引,适合离线数据和大对象。
07
XSS:三种类型与三层防御★
为什么"过滤关键词"永远不够
XSS 的本质是把数据当成了代码。防御不能靠黑名单过滤,要靠按上下文转义 + CSP 兜底 + HttpOnly 减少损失。
输入校验白名单,但只是第一层
→
输出转义按上下文转义,最关键的一层
→
CSP禁止内联脚本,兜底
→
HttpOnly就算被注入也偷不到 Cookie
- 转义必须看上下文:HTML 里要转
< > & ",属性里还要防引号闭合,JS 字符串里要转义反斜杠,URL 里要 encode。同一段数据在不同位置规则不同。 - CSP 是最有效的一层:
script-src 'self'之后,即使注入了<script>也执行不了。代价是要处理内联脚本和 eval。 - innerHTML 不等于必然有洞:如果内容是可信的静态模板就没问题。危险的是"把用户输入拼进去"。
- 富文本要单独处理:不能简单转义(那样格式全没了),要用 DOMPurify 这类库做白名单清洗。
面试怎么答XSS 分存储型、反射型、DOM 型,本质是把不可信数据当代码执行。防御按层来:输入做白名单校验,输出按上下文转义,用 CSP 禁止内联脚本兜底,Cookie 加 HttpOnly 限制被窃取的后果。DOM 型要避免把用户输入塞进 innerHTML。
08
CSRF 与 SameSite★
借你的登录状态,替你做请求
CSRF 能成立的前提是浏览器会自动带上 Cookie。所以防御的核心不是"识别用户",而是识别这个请求是不是用户自愿发的——用 token 或 Origin 校验。
不够的防御
- 只校验 Cookie 是否存在
- 用 GET 做写操作(连表单都不用)
- 把 token 放在 URL 里(会进日志和 Referer)
有效的防御
- SameSite=Lax / Strict
- CSRF token(一次性,服务端校验)
- 校验 Origin / Referer
- 敏感操作二次验证
- SameSite=Lax 是现在的默认值:跨站 POST 不带 Cookie,但顶层导航的 GET 会带。这一个默认值挡掉了大部分 CSRF。
- SameSite 不等于万能:Lax 下顶级导航的 GET 仍会带 Cookie,所以写操作绝不能用 GET。
- CSRF 和 XSS 的关系:XSS 能读到页面里的 token,所以 XSS 一旦成立,CSRF token 就失效了。两者要分开防。
面试怎么答CSRF 是利用浏览器自动携带 Cookie 的特性,诱导用户在自己已登录的站点上发起非预期请求。防御手段是 SameSite Cookie、CSRF token、校验 Origin/Referer,以及敏感操作二次验证。写操作不能用 GET。
09
渲染流程:五步,不是三步★
从 DOM 到屏幕上的像素
明确五步:构建 DOM 和 CSSOM → 合成渲染树 → 布局(Layout)→ 绘制(Paint)→ 合成(Composite)。JS 会阻塞 DOM 构建,CSS 会阻塞渲染树,这两条决定了优化方向。
DOM + CSSOM两棵树分别构建
→
Render Tree只含可见节点
→
Layout算位置和尺寸
→
Paint画成图层位图
→
Composite合成到屏幕
- display:none 的节点不进渲染树,但
visibility:hidden的会进——它占位,只是不画。 - CSS 阻塞的是渲染,不是解析:CSSOM 没建好之前浏览器不会渲染,因为不确定该画成什么样。所以关键 CSS 要内联或优先加载。
- JS 阻塞的是 DOM 构建:脚本可能改 DOM,所以必须等它执行完再继续解析。放在 body 末尾或用 defer 就是这个原因。
- Layout 是最贵的一步:它要算每个元素的位置,一旦某个元素尺寸变了,可能牵连整棵树。这就是"回流"贵的根源。
面试怎么答渲染分五步:解析 HTML 生成 DOM、解析 CSS 生成 CSSOM、合成只含可见节点的渲染树、计算布局、绘制并合成图层。CSS 阻塞渲染,JS 阻塞 DOM 解析,布局是最昂贵的一步。
10
重排、重绘与合成层★
改哪个属性,决定了要重走几步
改几何属性触发重排(最贵),改颜色触发重绘,改 transform/opacity 只触发合成(最便宜)。这就是"动画只用 transform"的全部理由。
◐ 可交互:同样是把方块挪过去,代价差十几倍
点一下,看它触发了哪些阶段
累计次数
回流 layout0
重绘 paint0
合成 composite0
估算主线程耗时 0 ms
三种改法在代码上几乎一样长,但走的流水线完全不同。
会触发重排(Layout)
- width / height / padding / margin
- top / left / position
- font-size / line-height
- 读 offsetTop / getBoundingClientRect(强制同步布局)
只触发合成(Composite)
- transform: translate / scale
- opacity
- filter(部分场景)
- 前提是元素在独立图层上
- 读取布局属性也会触发重排:为了让
offsetHeight返回准确值,浏览器必须立刻把之前的改动算完。这就是 layout thrashing(读写交替)的来源。 - will-change 有代价:它提前把元素提升为独立图层,图层要单独占显存。滥用会导致内存暴涨、合成变慢,动画结束后应该移除。
- 图层不是越多越好:每个图层都要单独栅格化,移动端上几十个图层就会明显掉帧。
面试怎么答改几何属性触发重排,改绘制属性触发重绘,改 transform 和 opacity 只走合成。性能优化的关键是把读操作和写操作分开,避免强制同步布局;动画优先用 transform,必要时用 will-change 提前提层但要注意内存代价。
11
事件:捕获、冒泡与 passive★
为什么滚动回调里不能随便 preventDefault
DOM 事件分三个阶段:捕获 → 目标 → 冒泡。事件委托利用冒泡把监听挂在父元素上。
passive: true 让浏览器不必等回调执行完就能开始滚动。为什么 passive 影响这么大
浏览器默认要等 touchmove 回调执行完,
才知道你会不会 preventDefault 阻止滚动。
回调里只要有一次同步计算耗时 100ms,
滚动就卡 100ms。
passive:true 之后浏览器直接开始滚,
不等回调。代价是回调里再调 preventDefault 会失效(并有警告)。
- Chrome 对 document 级 touchstart/touchmove 默认 passive:所以以前那种在 document 上 preventDefault 拦滚动的写法会失效,得显式写
passive: false。 - stopPropagation 只挡冒泡,不挡同元素的其他监听;
stopImmediatePropagation才会把同元素后面的也挡掉。 - 委托的边界:不冒泡的事件(focus、blur、mouseenter)用不了委托,要用 focusin/focusout 代替。
面试怎么答事件流是捕获、目标、冒泡三个阶段。事件委托利用冒泡在父元素统一处理,减少监听数量。passive 告诉浏览器回调不会阻止默认行为,让滚动不必等待 JS,这是移动端滚动性能的关键优化。
12
Service Worker 与离线缓存
一个跑在页面之外的代理
Service Worker 是独立于页面的线程,能拦截该作用域下所有请求。它的价值不在"离线",而在把缓存策略变成代码——你能决定每个请求怎么取。
四种常用策略,按资源类型选
Cache First HTML/CSS/JS/字体 命中缓存就走,适合带 hash 的静态资源
Network First 接口数据 先请求,失败回退缓存,适合时效性强的
Stale While Revalidate 头像、列表 先给旧的,同时后台更新
Network Only 支付、登录 绝不缓存
- 三种状态都要处理:install 时预缓存,activate 时清理旧版本缓存,fetch 时决定策略。少了 activate 那一步,旧缓存会一直堆着。
- 更新是个坑:SW 文件本身变了一字节,浏览器才会去装新版。所以"改了业务代码但缓存策略没变"时,用户可能一直拿旧资源——这就是 skipWaiting 和版本号存在的原因。
- 只在 HTTPS 下可用(localhost 除外),因为它的权限太大,能拦所有网络请求。
面试怎么答Service Worker 是独立线程,可以拦截作用域内的网络请求,实现离线缓存和自定义缓存策略。生命周期是 install、activate、fetch 三个阶段,要注意预缓存、清理旧版本和 skipWaiting 的更新问题。必须在 HTTPS 下运行。
13
安全响应头:几行配置挡住一大类攻击
比在代码里逐处防御更可靠
安全头属于兜底防线:代码里的转义可能漏,但 CSP 只要配对了,注入的脚本就是跑不起来。
- HSTS 是不可逆的:一旦设了 max-age,在有效期内浏览器强制用 HTTPS,即使你撤回头也会被浏览器记住。所以先用小值试。
- CSP 上线要用 Report-Only 先跑:
Content-Security-Policy-Report-Only只上报不拦截,看清有哪些违规再切正式。 - SRI 用于第三方脚本:
integrity="sha384-..."校验 CDN 返回的内容没被篡改,CDN 被投毒时能挡住。 - nonce 比 unsafe-inline 好得多:每次请求生成随机值,内联脚本带上这个属性才允许执行。
面试怎么答常见安全头:HSTS 强制 HTTPS、CSP 限制资源来源并禁止内联脚本、X-Content-Type-Options 禁止 MIME 嗅探、X-Frame-Options 防点击劫持、Referrer-Policy 控制信息泄露。CSP 上线前应该用 Report-Only 模式观察。
14
Performance API 与页面生命周期
别靠猜,浏览器自己会报
所有性能指标的真实来源都是 Performance API。理解它,就不用背"某个指标怎么算"——你能自己取。
- 用 PerformanceObserver 而不是轮询:它能在指标产生时回调,不影响主线程。LCP、CLS、INP 都靠它收集。
- 页面生命周期:
visibilitychange比unload可靠得多。unload和beforeunload会破坏往返缓存(bfcache),现在被建议弃用。 - 要在页面隐藏时上报:用
visibilitychange加navigator.sendBeacon,普通 fetch 可能来不及发出去。
面试怎么答Performance API 提供导航时间、资源加载、自定义 mark/measure,以及 PerformanceObserver 用来收集 LCP、CLS、INP 等指标。页面生命周期用 visibilitychange 判断页面是否可见,上报数据用 sendBeacon,避免用 unload 因为它会破坏 bfcache。
15
HTTP 状态码与请求方法★
面试里最爱考的十几个
状态码分五类,首字母就是类别:1 信息、2 成功、3 重定向、4 客户端错、5 服务端错。真正需要背熟的是十几个高频码,尤其301 与 302、304、401 与 403、502 与 504 这几对。
高频状态码,按"容易混"分组【重定向】
301 永久重定向,浏览器和搜索引擎都会缓存,慎用
302 临时重定向,每次都会重新请求
307 临时重定向,且保证方法和 body 不变(POST 还是 POST)
308 永久重定向,且保证方法不变
坑:301 缓存后用户改不回来,测试环境别用 301
【缓存】
304 Not Modified,协商缓存命中,响应没有 body
【客户端错】
400 请求参数有问题
401 未认证(没登录、token 过期)
403 已认证但无权限(登录了但没这个权限)
404 资源不存在
405 方法不允许(接口只支持 POST,用了 GET)
409 冲突(并发修改、唯一键重复)
422 参数格式对但语义不合法(校验失败常用)
429 请求太频繁,被限流
【服务端错】
500 代码抛异常
502 Bad Gateway,网关拿到上游的无效响应(上游进程挂了)
503 Service Unavailable,服务暂时不可用(过载、发布中)
504 Gateway Timeout,网关等上游超时
- 401 和 403 的界线:401 是"我不知道你是谁",403 是"我知道你是谁,但你不能做这个"。所以 token 过期返回 401 触发重新登录,权限不足返回 403 只提示。把 403 写成 401 会导致用户被反复踢出登录,这是很常见的实现错误。
- 502 和 504 的界线:502 是"上游给了个我看不懂的响应"(进程挂了、端口没监听),504 是"上游一直不回话"(超时)。所以 502 去看上游进程是否活着,504 去看上游为什么慢。
- 方法要遵守幂等性:GET / HEAD / PUT / DELETE 应该幂等,POST 不幂等。所以重试逻辑只能对幂等方法自动重试——重试一个 POST 可能会重复下单。
- OPTIONS 预检是什么:跨域的"非简单请求"浏览器会先发一个 OPTIONS 问服务端"我能用这个方法、带这些头吗",通过之后才发真实请求。所以后端必须处理 OPTIONS,否则跨域永远失败。
- 304 的坑:返回 304 时不能带 body,但可以带新的缓存头。如果服务端错误地返回了 body,某些客户端会解析出错。
面试怎么答状态码按首位分五类。高频里最容易混的是三对:401 是未认证,403 是已认证但无权限,把 403 写成 401 会让用户被反复踢出登录;502 是网关拿到上游的无效响应通常是上游进程挂了,504 是网关等上游超时,所以 502 查进程、504 查为什么慢;301 是永久重定向会被浏览器缓存,测试环境用 302 或 307 更安全。另外方法的幂等性决定了能不能自动重试,POST 不幂等,重试可能重复下单。
16
跨域:五种方案和前三种的死因★
CORS 只是其中一种
同源是协议 + 域名 + 端口三者全同。跨域限制是浏览器执行的,服务端之间调用不受限。五种方案:CORS、代理、JSONP、postMessage、WebSocket。现代项目 95% 用前两种,后三种各有明确的历史原因。
五种方案各自的适用面【CORS】服务端加响应头,正道
Access-Control-Allow-Origin: https://a.com
Access-Control-Allow-Credentials: true
带 Cookie 时 Origin 不能写 *,必须写具体域名
非简单请求会先发 OPTIONS 预检
【代理】开发用 devServer,生产用 Nginx
开发:vite 的 server.proxy / webpack 的 devServer.proxy
生产:Nginx 把 /api 转发到后端,浏览器看来是同源
优点:不用改后端,也不会有预检
这是企业项目最常用的做法
【JSONP】只支持 GET,靠 script 标签不受同源限制
服务端返回 callback({...})
死因:只能 GET、没有错误处理、有 XSS 风险
现在只存在于老接口
【postMessage】iframe 父子页面通信
跨域 iframe 之间唯一可靠的方式
必须校验 event.origin,否则等于开放给任何页面
【WebSocket】不受同源策略约束
但这不代表安全,服务端必须自己校验 Origin
- 简单请求和预检的分界:方法是 GET / HEAD / POST,且只带那几个安全头(Accept、Content-Type 限于三个值等),就是简单请求,直接发。否则先发 OPTIONS。所以"加了个自定义头就多一次请求",就是这个原因。
- 带 Cookie 的三个前提:前端
credentials: 'include'、服务端Allow-Credentials: true、以及Allow-Origin必须是具体域名不能是*。三个缺一个 Cookie 就带不上。 - 预检结果要缓存:
Access-Control-Max-Age设大一点(如 86400),否则每个请求都多一次 OPTIONS,接口多的时候很浪费。 - 为什么代理能绕开跨域:跨域是浏览器对"页面发起的请求"的限制。代理是服务端转发,浏览器只看到"请求自己的同源地址",所以没有跨域问题。这也解释了为什么生产环境总用 Nginx 转发而不是配 CORS。
- document.domain 已经废弃:
document.domain = 'a.com'这种老写法在现代浏览器里被移除,面试提到"已废弃"是加分点。
面试怎么答同源是协议、域名、端口全同,跨域限制由浏览器执行,服务端之间调用不受限。五种方案里 CORS 是正道,服务端加响应头,非简单请求会先发 OPTIONS 预检;代理是开发用 devServer、生产用 Nginx 转发,浏览器看来同源,所以企业项目最常用;JSONP 只能 GET 且没有错误处理已经基本淘汰;postMessage 用于跨域 iframe 通信,必须校验 origin;WebSocket 不受同源约束但服务端要自己校验 Origin。带 Cookie 时 Allow-Origin 不能写星号。
17
速答清单
考前十分钟只看这页
01输入 URL 之后发生了什么?
解析 URL → 各级缓存 → DNS → TCP 握手 → TLS → HTTP 请求 → 解析构建 DOM/CSSOM → 布局绘制 → 加载子资源。挑 DNS 或 TLS 展开。
02强缓存和协商缓存?
强缓存 Cache-Control: max-age 命中不发请求;协商缓存靠 ETag 返回 304。工程上 HTML 用 no-cache,资源用 hash + 一年。
03no-cache 和 no-store?
no-cache 可以存但用前必须校验;no-store 完全不许存。
04HTTP/2 解决了什么,没解决什么?
解决了应用层队头阻塞(多路复用)和头部冗余(HPACK),但共用一个 TCP 连接,丢包仍会阻塞所有流。
05同源策略限制什么?
限制跨源读取响应和访问 DOM,不限制请求发送——这是 CSRF 成立的前提。
06XSS 怎么防?
按上下文转义 + CSP 禁止内联脚本 + Cookie 加 HttpOnly。别靠关键词过滤。
07CSRF 怎么防?
SameSite Cookie、CSRF token、校验 Origin/Referer。写操作不能用 GET。
08渲染五步?
DOM/CSSOM → 渲染树 → 布局 → 绘制 → 合成。CSS 阻塞渲染,JS 阻塞 DOM 解析。
09为什么动画用 transform?
只触发合成,跳过布局和绘制。改 width/top 会触发重排,代价最大。
10passive 有什么用?
告诉浏览器回调不会 preventDefault,滚动不必等 JS 执行完,移动端滚动性能的关键。
11浏览器为什么要用多进程架构?
三个原因:稳定性,一个页面崩溃或 OOM 不会影响其他标签页;安全,渲染进程跑在沙箱里,需要特权的操作都要通过 IPC 请主进程代办;性能,多个页面可以真正并行。代价是每个进程都要带一份引擎基础开销,内存占用高。
12一个标签页就是一个进程吗?
不严格。Chrome 会做进程复用,同一个站点的多个标签页可能共享一个渲染进程以省内存;不同站点则强制隔离,这是 Site Isolation 策略。跨站的 iframe 也会被放进独立进程,父子之间只能 postMessage。
13JS 是单线程的,那浏览器也是单线程吗?
不是。浏览器是多进程的,渲染进程内部也是多线程的:主线程负责解析、样式、布局和 JS 执行,合成线程负责滚动和合成动画,还有光栅线程和若干 Worker 线程。说 JS 单线程指的是每个渲染主线程只有一个 JS 执行栈。
14为什么 transform 动画比改 left 更流畅?
因为 transform 的合成可以在合成线程完成,不占用主线程。改 left 会触发布局和重绘,必须回到主线程算,JS 一忙就掉帧。