
浏览器是我们最常用的软件之一,但它背后的工作机制往往容易被忽略。理解浏览器如何完成一次页面加载,不仅能帮助我们写出更高效、更稳定的前端代码,也能在排查性能问题、白屏问题和卡顿问题时更快定位根因。本文以 Chrome / Chromium 为例,串起浏览器的多进程架构、网络请求流程和渲染流水线。
现代浏览器不是一个单进程程序,而是由多个相互协作的进程和线程共同完成工作的。
Chrome 常见的进程包括:
这种架构带来三点好处:
当我们在地址栏输入一个 URL 后,浏览器并不是立刻去“请求网页”,而是会按一条比较完整的链路逐步完成资源获取。
浏览器会先解析输入内容,判断它是搜索词还是 URL,并拆分出协议、域名、端口、路径、查询参数等部分。
例如:
https://www.example.com:443/path?a=1
会被拆成协议 https、主机名 www.example.com、端口 443、路径 /path 和查询参数 a=1。
域名需要先解析为 IP 地址,浏览器才能找到目标服务器。
DNS 查询通常会依次参考这些来源:
这里要注意两点:
拿到 IP 后,浏览器会与服务器建立连接。
对于 TCP 来说,经典握手过程是:
SYNSYN + ACKACK这样双方确认了彼此的收发能力,连接建立完成。
如果是 HTTPS,还要先完成 TLS 协商。这里的目标不是“让通信变快”,而是建立一条加密、完整、可验证的安全通道。
简化后的过程可以理解为:
补充说明:
连接建立后,浏览器会发送 HTTP 请求,服务器返回 HTTP 响应。
请求通常包含:
Host、Cookie、Accept、User-Agent、Cache-Control 等POST、PUT 中提交的数据响应通常包含:
200、301、302、304、404、500 等Content-Type、Cache-Control、ETag、Set-Cookie 等这里还有一个很重要的点:缓存判断并不只发生在“请求之前”。浏览器会结合强缓存和协商缓存决定是否直接用本地资源,还是发起条件请求。
Cache-Control、Expires 判断资源是否仍可直接使用。ETag / If-None-Match、Last-Modified / If-Modified-Since 和服务器确认资源是否变化。如果服务器返回 304 Not Modified,说明资源没变,浏览器可复用本地缓存。
拿到 HTML 后,渲染进程会开始把字符串变成可见页面。这个过程不是一次性完成的,而是多个阶段协同推进。
浏览器会边下载边解析 HTML,把文本流转成结构化节点,最终生成 DOM 树。
例如:
<div class="root">Hello</div>
会被解析为一个包含元素节点、属性和文本节点的 DOM 结构。
浏览器还会启用 预解析器(preload scanner),在主解析器继续处理 HTML 的同时,提前发现外部 CSS、JS、图片等资源并并发下载,这能显著减少等待时间。
CSS 会被解析为 CSSOM。DOM 和 CSSOM 合并后,才能决定页面上每个可见元素应该如何显示。
这里要澄清一个常见误区:
遇到 <script> 时,浏览器会交给 JS 引擎执行。
JS 之所以重要,是因为它可以修改 DOM、修改样式、创建节点、触发重排重绘,甚至重新安排页面的最终结构。
几个常见结论:
defer 会在 HTML 解析完成后、DOMContentLoaded 之前执行,适合依赖 DOM 的脚本。async 会尽快下载并尽快执行,执行时机不固定,适合彼此独立的脚本。DOM 和 CSSOM 结合后,浏览器会生成 渲染树(Render Tree)。
渲染树只包含真正参与页面显示的节点:
display: none 的节点不会进入渲染树。visibility: hidden 的节点通常仍保留布局空间,只是不可见。布局阶段会计算每个元素的几何信息:
任何会影响几何信息的变化,通常都会触发 重排(reflow / layout)。
浏览器会把页面拆成若干图层,并决定哪些内容需要单独合成。
常见会促成分层的因素包括:
position: fixedtransformopacitywill-change分层的目的不是“越多越好”,而是把变化频繁的内容单独隔离出来,减少每次更新都重画整页的成本。
绘制命令会被转换成实际像素。为了提升效率,页面通常会被切成很多小块(tile),再并行交给光栅化线程或 GPU 处理。
最后,多个图层会在合成阶段重新拼装,形成最终画面并提交到屏幕。
这一步通常由合成线程、GPU 进程和显示系统共同完成。对开发者来说,动画只改 transform 或 opacity,往往就是因为它更容易走合成路径,开销更小。
真正的“打开一个网页”不只是发请求和渲染页面。浏览器还要先完成一次导航,决定用哪个进程处理页面,再逐步进入渲染流程。
当用户输入 URL 或点击链接时,浏览器进程会先接管这次导航,处理地址栏输入、历史记录、权限检查和站点策略。
它通常会先做几件事:
浏览器不会永远只用一个渲染进程。
这也是为什么“一个标签页”并不总等于“一个固定进程”。
所谓关键渲染路径,就是页面从“拿到最初资源”到“首屏真正能看到内容”所经历的最短有效链路。
它通常最受这些因素影响:
如果想提升首屏速度,核心思路不是“把所有东西都提前加载”,而是优先保证首屏真正需要的资源先到位。
常见的页面体验指标里,比较关键的两个阶段是:
它们都和关键渲染路径紧密相关。FCP 更像“页面开始有东西了”,LCP 更像“核心内容真正可见了”。
浏览器原理里,事件循环几乎是绕不开的一环。页面为什么会卡顿、为什么某些代码会延后执行、为什么微任务总是先于下一帧渲染,答案都在这里。
JavaScript 在浏览器中不是孤立运行的,它要和任务队列协作。
Promise.then、queueMicrotask、MutationObserver 回调等。一般来说,每执行完一个宏任务,浏览器会先清空当前微任务队列,再考虑是否进入渲染阶段。
微任务的优先级通常高于下一轮宏任务,这意味着:
JavaScript 不只是“改数据”,它还能直接影响页面计算。
常见情况包括:
offsetWidth、getBoundingClientRect() 这类布局相关属性时,浏览器可能被迫先完成一次布局计算。所以,浏览器里的“执行快”不等于“体感快”,真正重要的是别长期占住主线程。
浏览器之所以采用多进程,不只是为了稳定,也是在做安全隔离。
同源策略是浏览器安全模型的重要基础。协议、域名、端口只要有一个不同,通常就会被视为不同源。
它限制了页面直接读取跨源资源的能力,从而降低站点之间互相窃取数据的风险。
Chrome 会尽量把不同站点的页面隔离在不同进程里,减少恶意页面越权访问其它站点数据的可能。
它们分别解决的是“我能加载什么”和“我能读什么”。
理解浏览器工作流的价值,最终还是要落回到“怎么写得更快、更稳”。
合理使用缓存
Cache-Control、ETag、Last-Modified 等缓存策略。减少请求开销
优化传输
避免阻塞主线程
defer。减少重排和重绘
transform 和 opacity。谨慎使用图层优化
will-change、translateZ(0)。拆包与懒加载
减少主线程压力
使用合适的格式
响应式加载
srcset 和 <picture> 适配不同屏幕密度和尺寸。预加载
preload 适合提前加载当前页面马上要用的关键资源。prefetch 适合提前准备未来可能用到的资源。服务端渲染与 hydration
在同一个进程里,浏览器也不是“一个线程干所有事”。不同线程各司其职,才能让页面既能渲染,又能响应用户操作。
主线程通常负责最核心、最敏感的工作:
这也是为什么主线程一忙,页面就容易卡。
合成线程负责把不同图层组合起来,尽量减少主线程参与。
如果动画只改变 transform 或 opacity,很多时候就可以更多依赖合成阶段来完成。
栅格线程会把绘制命令转成像素数据,并行处理页面切块。
JS 的执行虽然和渲染紧密相关,但从浏览器角度看,它更像是在主线程上不断插入任务。也正因为如此,长任务、频繁同步计算、过深的微任务链,都可能直接伤到流畅度。
缓存不是一个东西,而是一组分层机制。把它们分清楚,排查加载问题会轻松很多。
它们之间不是互斥关系,而是会按浏览器策略共同参与资源命中。
协议升级的核心目标,不只是“更快”,而是更稳地利用连接和减少阻塞。
如果要判断浏览器加载和渲染是否真正变好,最好不要只看“感觉快不快”,而是看指标。
这些指标能把“页面慢”拆成更具体的问题:是网络慢、渲染慢、脚本慢,还是交互响应慢。
浏览器的一次页面加载,本质上是 URL 解析、DNS 查询、连接建立、请求响应、HTML/CSS/JS 解析、布局、绘制、合成 的完整链路。
如果把这条链路吃透,很多常见问题都会变得更容易解释:
理解浏览器,不只是理解“页面怎么显示出来”,更是理解前端性能、稳定性和用户体验是如何一步步被建立起来的。