浏览器架构与工作原理

浏览器(Web Browser,网页浏览器)是用于检索、展示以及传递 Web 信息资源的应用程序。Web 资源通过统一资源标识符(URI)进行定位,资源可以是网页、图片、音视频,或是任意可在 Web 环境中呈现的内容。用户可以借助超链接,在不同的信息资源之间完成浏览跳转。

浏览器发展历程

世界上第一个浏览器

1990 年,蒂姆·伯纳斯-李(Tim Berners-Lee)开发出世界上第一款网页浏览器 WorldWideWeb,为了避免和万维网 World Wide Web 概念重名,后续改名为 Nexus。早期浏览器能力十分有限,仅能够支持基础文本、简单样式以及部分格式的图片与音频。

Erwise 是第一款面向普通用户、具备图形GUI界面的网页浏览器,由罗伯特·卡里奥主导开发。

第一次浏览器大战

1993 年,马克·安德森发布 Mosaic 浏览器,这款浏览器极大降低了 Web 的使用门槛,直接推动了互联网的普及。安德森曾是 NCSA 的 Mosaic 项目核心成员,离开之后创立 Netscape 公司,推出基于 Mosaic 技术的 Netscape Navigator,这款浏览器迅速占领市场,市场占有率一度高达 90%。

为追赶互联网浪潮,微软收购 Spyglass 公司的相关技术,推出 Internet Explorer 浏览器,由此拉开第一次浏览器大战。凭借 Windows 操作系统的捆绑分发优势,Internet Explorer 市场份额持续扩张,在 2002 年市场占有率超过 95%,第一次浏览器大战宣告结束。

第二次浏览器大战

在微软IE的挤压之下,Netscape的市场处境持续恶化。网景没有就此放弃,1998年成立Mozilla基金会,开启新一代浏览器的研发工作。

2003年,苹果随Mac OS X Panther系统发布Safari浏览器;受限于Mac设备的用户基数,此时Safari影响力有限。2005年苹果正式开源Safari的渲染引擎WebKit,这件事对后续整个Web行业的发展产生了深远影响。

2004 年,Mozilla 发布 Firefox 浏览器,内核采用开源的 Gecko。浏览器扩展生态完善,功能丰富,一经发布就收获大量用户,市场份额稳步提升。

2008年,Google基于WebKit开源代码,创建Chromium项目,Chromium承担新技术试验的角色,用来验证各类前沿Web特性。同年面向普通消费者发布Chrome浏览器,Chrome基于Chromium的稳定版本构建,额外集成部分私有媒体解码器。

至此,IE依托Windows保持庞大存量,Firefox与Chrome不断蚕食IE的市场,浏览器市场形成三足鼎立的格局。

备注:Chromium面向开发者,Chrome面向普通终端用户,二者同源但定位不同。

2010 至今

  • 2010 年:苹果发布 WebKit2,对架构进行改造,引入多进程模型。
  • 2013 年:谷歌与苹果在 WebKit 发展方向上出现分歧。4 月谷歌宣布独立开发 Blink 渲染引擎,Blink 最初直接复制 WebKit 的代码库,随后逐步清理无关代码,进行大规模架构改造。
  • 2015 年:Windows 10 系统发布,微软推出初代 Microsoft Edge 浏览器,使用自研 EdgeHTML 内核,目标逐步替代 IE 浏览器。
  • 2017 年:Mozilla 推出 Firefox Quantum 版本,对浏览器整体架构大规模重构,重点优化多进程、内存占用与渲染性能。

  • 2018 年:Chrome 67 正式开启 Site Isolation(站点隔离)安全机制;微软正式对外宣布,新版 Edge 将放弃自研 EdgeHTML 内核,切换到 Chromium 的 Blink 内核。

  • 2019 年:基于 Chromium 内核的新版 Microsoft Edge 正式发布,支持 Windows、macOS、Windows 7 多平台;WebAssembly、WebGPU 相关 Web 标准持续迭代完善。
  • 2021 年:Firefox 上线 Fission 跨源站点隔离能力;微软停止对旧版 EdgeHTML 内核 Edge 的技术支持;微软旗下 365 系列云产品逐步停止对 IE11 的访问支持。
  • 2022-06-15:IE11 桌面版正式停止官方安全更新与维护;Windows 系统逐步移除 IE 的入口,企业遗留业务可以依靠 Edge 浏览器的 IE 兼容模式做过渡。
  • 2023-至今:Chrome 保持市场主导地位;Safari 持续完善 Web 标准兼容性、增强 PWA 能力;Firefox 继续维护 Gecko 内核;各大浏览器厂商共同推进 TLS 1.3、HTTP/3(QUIC)以及 Fugu 系列 Web 新能力落地。

备注:IE浏览器已经完成历史使命,现代主流浏览器为 Chrome、Chromium版Edge、Firefox、Safari。

浏览器基础概念

从结构上,浏览器可以划分为外壳 Shell 与内核两大部分,其中外壳种类繁多,而内核的实现方案相对有限。Mozilla 将 Gecko 内核独立拆分之后,外壳与内核才形成了清晰的边界。

外壳 Shell 就是浏览器的外层UI,包含菜单、工具栏、设置面板等,负责接收用户操作交互,所有页面渲染能力都需要调用内核来完成;内核负责网页解析渲染。

早期 Gecko 是单进程浏览器模型,现代主流浏览器(Chrome/Blink、新版Firefox/Gecko、Safari/WebKit2)已经升级为多进程沙箱架构

经典架构

参考早期 Firefox(Gecko)经典架构,可以在逻辑层面将浏览器内部划分为七大模块:

  1. 用户界面:地址栏、前进后退按钮、书签菜单栏等。除网页主内容区域之外,浏览器所有可见交互部分,都归属于用户界面。
  2. 浏览器引擎:承担调度工作,在用户界面与呈现引擎之间转发指令。
  3. 呈现引擎:也常被称为渲染引擎,严谨定义下的浏览器内核,负责解析网页资源并完成页面渲染。如果资源是 HTML 文档,它就会解析 HTML、CSS,构建 DOM、CSSOM,完成样式计算、布局、绘制,最终把内容渲染到屏幕。
  4. 网络模块:负责处理 HTTP 等网络请求,同时处理缓存逻辑,对外提供一套与操作系统平台无关的网络调用接口。
  5. 用户界面后端:用于绘制基础控件,例如输入框、下拉框、窗口;对外暴露跨平台通用接口,底层再调用操作系统原生的 UI 能力。
  6. JavaScript 引擎:完整负责 JavaScript 代码的处理与执行。其内部包含解析器、解释器、JIT 即时编译器、垃圾回收器;其中JavaScript 解析器只是 JS 引擎的子组件,仅负责将 JS 源码解析生成 AST 抽象语法树,本身不执行代码
  7. 数据存储模块:浏览器本地持久化层,在磁盘保存 Cookie、Storage 等各类数据;HTML5 还新增了 IndexedDB 这类浏览器内数据库能力。

术语区分:严谨定义中,浏览器内核特指渲染引擎,负责解析网页语法,决定网页的渲染表现。行业口语习惯会把渲染引擎JS 引擎合在一起统称为浏览器内核,二者深度绑定、协同工作,但 JS 引擎在架构上属于独立模块,并非渲染引擎内部的子单元。不同浏览器内核对于 Web 标准的实现存在差异,因此开发需要针对多内核环境做兼容性测试。

浏览器进程模型

早期浏览器采用单进程架构,页面渲染、JS 执行、网络请求、UI 交互、插件全部运行在同一个进程内部。任意一个页面发生卡死、崩溃,会导致整个浏览器崩溃;JavaScript 执行会直接阻塞页面渲染;第三方插件稳定性差,极易引发浏览器整体闪退。

现代主流浏览器(Chrome/Blink、新版Firefox/Gecko、Safari/WebKit2)已经升级为多进程沙箱架构:七大模块对应的功能职责全部保留,但不再全部驻留在同一个进程,逻辑模块被拆分部署到浏览器主进程、渲染进程、网络进程、存储服务进程等不同实体进程,依靠 IPC 进程间通信完成协同。

Chrome 主要进程:

  1. 浏览器主进程(全局唯一):浏览器入口主进程,管理浏览器UI界面、地址栏、书签;统一管理所有子进程;处理网络调度、文件访问、下载行为;负责各个子进程的创建与销毁。
  2. 渲染进程(每个标签页独立):每一个标签页默认分配独立渲染进程;同源页面会做进程复用,开启 Site Isolation 站点隔离特性后,不同源页面强制分配不同渲染进程。
  3. GPU进程(全局唯一):负责GPU硬件加速相关工作,处理3D绘制、CSS transform、图层合成;全部标签页共享同一个GPU进程。
  4. 插件进程:专门用于运行NPAPI类型插件,典型代表是旧版Flash;Flash技术已经全面淘汰,该进程现在几乎不再启用。
  5. 网络进程:独立进程,专门处理HTTP/HTTPS请求、DNS域名解析、HTTP缓存逻辑,和渲染进程相互独立。
  6. 扩展进程:浏览器扩展程序独立运行的进程,隔离扩展代码,避免恶意扩展代码干扰页面运行。

多进程架构优势:

  • 单个标签页崩溃,仅当前渲染进程终止,其余页面、浏览器 UI 不受影响;
  • 进程之间内存隔离,沙箱机制提升整体安全性;
  • 在多核 CPU 设备上,多个进程可以充分利用多核并行计算。

浏览器渲染进程

渲染进程是现代多进程浏览器的沙箱隔离进程,呈现引擎(渲染引擎)与 JS 引擎都运行在渲染进程内部。一个页面标签通常对应一个渲染进程(部分站点会做进程复用),进程内部拥有多条线程:主线程、合成线程,以及若干工作线程

渲染进程处于安全沙箱之内,不允许直接访问操作系统资源,网络请求、磁盘读写、弹窗、文件操作等系统能力,都必须通过 IPC 委托浏览器主进程代为执行。

渲染进程内部各线程:

  • 主线程:渲染进程最核心的线程,绝大部分页面逻辑在这里执行,任务繁重,一旦阻塞会直接造成页面卡顿、无响应

    • 解析 HTML 文档,构建 DOM 树;

    • 解析 CSS(外部样式表、style 标签、行内样式),构建 CSSOM 样式对象模型;

    • DOM + CSSOM 合并生成渲染树 Render Tree;

    • 回流:计算渲染树上每个节点的几何位置、尺寸;

    • 重绘:把各个节点按照样式填充颜色、图片、阴影等绘制到图层上;

    • 执行 JavaScript 代码;

    • 处理 DOM 事件:点击、输入、滚动事件、定时器、回调等;

    • 处理页面的 DOM 操作、样式修改。

  • 合成线程:合成线程独立于主线程,属于渲染进程内部线程,专门负责合成阶段,是实现流畅动画、滚动的关键。

    • 对绘制完成的页面进行图层分块,把页面切分为多个瓦片(Tile);

    • 将图层瓦片提交给 GPU,由 GPU 完成图层合成;

    • 处理 transform、opacity 这类只触发合成,不触发布局、重绘的动画;

    • 响应滚动行为:滚动时直接移动图层,不需要主线程参与,因此可以做到丝滑滚动;

    • 接收来自主线程的图层信息,负责更新页面最终画面。

  • 各类工作线程(Worker 系列线程):工作线程脱离主线程运行,不能操作 DOM,用来承担耗时代码,避免阻塞主线程,属于渲染进程内部子线程。

    • Web Worker:普通工作线程,执行大量计算、数据处理逻辑,无法访问 DOM、window 对象;只能通过 postMessage 和主线程通信。

    • Service Worker:特殊 Worker,独立于页面生命周期,可以拦截网络请求、做离线缓存,归属于渲染进程环境,但生命周期由浏览器主进程管理。

    • Shared Worker:可被多个同源页面共享的工作线程。

渲染引擎

渲染引擎获取HTML、XML、图片等网页资源,结合CSS样式信息,计算页面布局,最终输出内容到显示器或者打印机。不同渲染引擎对Web标准的实现存在区别,最终页面渲染效果会存在差异,网页浏览器、邮件客户端等需要展示富文本内容的软件,都会依赖渲染引擎。

业界主流渲染引擎一共有四种:Gecko、Webkit、Trident、Blink。

Gecko

Gecko从Netscape6版本开始投入使用,后续Mozilla Firefox浏览器持续沿用这套内核。Gecko完全开源,全世界开发者都可以参与代码贡献、扩展能力,这也是它能够快速发展的重要原因。

Firefox是使用Gecko最具代表性的浏览器,因此Gecko也被俗称为Firefox内核。Gecko具备良好的跨平台特性,可以运行在Windows、BSD、Linux、Mac OS X多个操作系统。

Webkit

Safari是苹果推出的浏览器,Webkit内核来源于Linux桌面项目KDE的KHTML。2003年Safari发布,后续成为Mac OS、iOS设备的默认浏览器,同时也提供Windows平台版本。

当年苹果在对比 Gecko 与 KHTML 之后选择 KHTML,看中它清晰的代码结构和优秀渲染速度,在此基础上衍生出 WebKit。WebKit 的开源,也是苹果对 Web 生态非常重要的贡献。

注意:Webkit一般指代渲染引擎;Safari配套的JavaScript引擎为Nitro(基于JavaScriptCore),二者相互独立。

2008年谷歌发布Chrome浏览器,底层项目为Chromium。Chromium早期基于WebKit开发,谷歌对WebKit源码做大量梳理重构,因此Chromium渲染表现和其他WebKit系浏览器会存在细微差异。

2013年谷歌正式公布Blink引擎,Blink是WebKit的分支项目,由谷歌与Opera Software共同维护。

背景原因:WebKit2采用全新多进程架构,这套架构和Chromium现有的沙箱模型存在冲突。Chromium很难直接合并WebKit2的改动,项目维护复杂度持续增加,因此谷歌决定 fork 出独立 Blink 内核,删除大量历史冗余代码,做轻量化改造。网传 Blink 移除了 880 万行 WebKit 历史代码。

Trident(废弃)

Trident内核在1997年的IE4当中首次投入使用,基于Mosaic相关代码二次开发,一直延续使用到IE11,也就是大家口中常说的IE内核

因为Windows系统的预装分发,Trident曾经占据极高市场份额,但是微软长期更新迭代缓慢,带来两个比较突出的问题:

  • 很长一段时间内 Trident 对 W3C Web 标准的支持存在脱节;
  • 内核积累大量Bug,安全漏洞得不到及时修复。

大量开发者诟病 IE 的兼容性,间接推动 Firefox、Opera 这类浏览器兴起。

IE11开始支持WebGL;IE8的JS引擎为JScript,IE9升级为Chakra,两者能力差距巨大,Chakra在执行性能、标准化支持上都有明显提升。

Windows10初代Edge浏览器使用的EdgeHTML内核,是Trident的现代化改造版本,该内核现已完全废弃。

Presto(废弃)

Presto 是 Opera 浏览器自研渲染引擎,早期 Opera 7 开始使用,对渲染性能做了深度优化,代价是对部分网页兼容性表现不佳。

2013年Opera宣布放弃Presto,迁移到基于WebKit的Chromium架构;在Chrome推出Blink之后,Opera也跟进切换到Blink内核。这次内核切换带来不少负面问题,原有轻量化优势不再,同时书签等数据迁移工作耗时两年,流失大量老用户。

JS 引擎

JS引擎的职责是解析并执行JavaScript代码,实现网页的各类动态交互逻辑。

早期渲染引擎和JS引擎耦合在一起,随着JS应用复杂度提升,JS引擎逐步独立出来,现在提到浏览器内核,大多特指渲染引擎。

主流浏览器对应的JavaScript引擎:

  • V8:C++编写,开源,谷歌丹麦团队开发,用于Chrome浏览器,同时也是Node.js的JS引擎。
  • JavaScriptCore:开源,WebKit系浏览器(Safari)使用;2008升级为SquirrelFish,苹果内部代号Nitro。
  • Rhino:Mozilla基金会维护,完全使用Java编写,多用于HTMLUnit测试环境。
  • SpiderMonkey:世界第一款JavaScript引擎,Netscape时代诞生,现在为Firefox所用。
  • Chakra(JScript):用于IE11浏览器。
  • Chakra(JavaScript):初代EdgeHTML版本Edge浏览器使用。
  • KJS:KDE项目ECMAScript引擎,Konqueror浏览器使用。

主流浏览器

现代生产环境主流浏览器为Chrome、新版Edge(Chromium)、Firefox、Safari,IE系列全部已经停止官方维护。

各浏览器对应的内核信息:

  • IE:Trident 内核(已退役)
  • Chrome:Chrome27及更早版本使用Webkit;Chrome28之后使用Blink内核
  • Firefox:Gecko 内核
  • Safari:Webkit 内核
  • Opera:早期 Presto → 短暂 WebKit → 现在 Blink 内核
  • 旧版 Edge(2015-2020):EdgeHTML;新版 Edge:Blink 内核
  • 360、猎豹浏览器:Trident + Blink双内核
  • 搜狗、遨游、QQ浏览器:Trident(兼容模式)+ Webkit(高速模式)
  • 2345:旧版本依赖IE内核,新版本切换IE+Blink双内核
浏览器 内核(渲染引擎) JavaScript 引擎
Chrome Blink(28~) Webkit(Chrome 27) V8
Firefox Gecko SpiderMonkey
Safari Webkit JavaScriptCore
初代Edge EdgeHTML Chakra(for JavaScript)
IE11 Trident Chakra(for JScript)
PhantomJS Webkit JavaScriptCore
Node.js - V8

工作原理

从浏览器地址栏输入URL并回车,到页面完整展示在屏幕,整个过程分为五大阶段:

  1. 导航
  2. 响应
  3. 解析
  4. 渲染
  5. 交互

导航

导航阶段:网页加载的起点,触发场景包含:地址栏输入网址、点击页面超链接、提交表单等。

DNS 查找

导航第一步,需要把域名解析成服务器真实IP地址。

浏览器获取IP会按照缓存优先级依次查找:

  1. 浏览器DNS缓存;
  2. 操作系统hosts本地配置文件;
  3. 路由器DNS缓存;
  4. ISP运营商本地DNS缓存;
  5. 递归查询:根域名服务器 → 顶级域名服务器 → 权威域名服务器,最终拿到目标服务器IP。

备注:解析结果会做缓存,后续访问同一域名可以复用缓存结果,减少查询耗时。在移动网络环境中,DNS解析带来的延迟会表现得更加明显。

TCP 三次握手

拿到服务器 IP 之后,浏览器通过 TCP 三次握手和服务端建立可靠的字节流传输连接。

TCP 是面向连接的可靠传输协议,握手用于建立连接,挥手用于释放连接。

核心目标:确认双方收发报文能力正常、协商双方初始序列号 ISN,保障后续数据完整可靠交互。

TCP 头部拥有 6 个 1bit 标志位:SYNACKFINRSTPSHURG,每个占 1 比特,置 1 代表标志生效。

  • ISN(Initial Sequence Number)初始序列号:TCP 建立连接时,客户端与服务端各自生成随机的初始序列号。
  • SYN(Synchronize)同步位:请求建立连接,携带本方初始序列号。
  • ACK(Acknowledgment)确认位:确认已经收到对端的数据。
  • FIN(Finish)结束位:本方不再发送新数据,请求关闭本方向发送通道。

MSL(Maximum Segment Lifetime)是报文最大生存时间:报文在网络中允许存活的最长时间;2MSL = 2 × MSL

建立连接:

  • 第一次握手:客户端 → 服务端,发送 SYN 报文,携带客户端初始序列号x。确认客户端发送能力正常。
  • 第二次握手:服务端 → 客户端,回复 SYN-ACK 报文。ACK 确认客户端序列号 x,同时 SYN 携带服务端自身初始序列号 y。确认服务端接收、发送能力均正常。
  • 第三次握手:客户端 → 服务端,回复 ACK 报文,确认服务端序列号y。确认客户端接收能力正常。

服务端收到该 ACK 之后,连接进入 ESTABLISHED 就绪状态,可以传输业务数据。

TLS 四次握手(HTTPS)

HTTP 直接基于 TCP 传输明文数据,HTTPS = HTTP + TLS

TCP连接建立完成之后,会执行TLS握手,完成服务器身份校验、协商加密套件、生成会话密钥,后续全部HTTP数据都会加密传输,实现防窃听、防篡改、身份认证。

HTTP/1.1、HTTP/2:先完成 TCP 三次握手建立 TCP 通道,再在 TCP 字节流之上执行 TLS 握手。

HTTP/3:运行在 QUIC(UDP)之上,TLS1.3 握手与传输握手集成,不存在 “先建立 TCP 连接,再执行 TLS” 两段式模型。

TLS 分为两层:

  • 握手协议:负责协商会话信息、校验身份、协商密钥材料;
  • 记录协议:负责对上层业务数据做加密分片传输。

TLS 1.2 完整握手(历史版本,2-RTT)

  • ClientHello(客户端→服务端):客户端上报支持的 TLS 版本、密码套件列表、客户端随机数。
  • ServerHello + Certificate + ServerKeyExchange (可选) + ServerHelloDone(服务端→客户端):服务端选定 TLS 版本、加密套件,返回服务端随机数,下发服务器 CA 证书链。
  • ClientKeyExchange + ChangeCipherSpec + Finished(客户端→服务端):客户端校验证书合法性(签名校验、域名匹配、有效期校验);证书校验失败浏览器直接抛出证书告警,阻止访问。客户端使用服务端公钥加密预主密钥;切换加密模式,发送加密的 Finished 消息。
  • ChangeCipherSpec + Finished(服务端→客户端):双方基于随机数与预主密钥推导会话密钥;服务端切换加密模式,返回加密 Finished 消息。双方 Finished 消息校验握手完整性,握手完成,开始加密传输 HTTP 报文。

TLS 1.3(现代主流,1-RTT):现代浏览器优先使用 TLS 1.3,握手优化为 1 个 RTT,大幅降低 HTTPS 连接延迟;移除 RSA 密钥交换,精简握手消息,安全性与性能都得到提升。

响应

响应阶段:连接建立完成,浏览器发送HTTP GET请求获取HTML资源;服务器接收请求之后,返回HTTP响应头与HTML响应体内容。

解析

解析阶段:浏览器将网络传输过来的字节流,转换为DOM树、CSSOM树这两套内部数据结构,供渲染引擎后续使用。

构建DOM树

HTML解析器对HTML文本做分词处理,一步步构建DOM节点树。

当解析器遇到图片这类非阻塞资源,浏览器会发起网络请求,同时继续解析HTML文档。

但是遇到script标签(未设置async、defer)的时候,会暂停HTML解析,交出控制权给JS引擎执行脚本。浏览器的预加载扫描器可以提前识别资源链接发起请求,缓解脚本阻塞带来的性能问题,但大量JS脚本依然是页面性能瓶颈。

构建CSSOM树

解析CSS样式文件,生成CSSOM(CSS对象模型)。DOM和CSSOM是两套相互独立的数据结构。浏览器遍历全部CSS规则,以CSS选择器为依据,构建出具备父子、兄弟层级关系的样式节点树。

渲染

渲染阶段:合并DOM树和CSSOM树,生成渲染树,依次执行样式计算、布局、绘制,部分场景还会执行图层合成。

样式计算

结合DOM树与CSSOM树,生成Render渲染树;从根节点遍历,只收集页面上可见的DOM节点。

布局计算

在渲染树上执行布局,计算每一个元素的几何信息:元素宽高、在页面当中的坐标位置。

页面第一次计算元素几何信息,称为布局;当页面样式改变,浏览器重新计算布局,这个过程就叫做回流(重流 reflow)

绘制

把布局得到的几何信息,转换成屏幕上真实的像素。绘制会填充元素的文本、颜色、边框、阴影、图片等全部可视内容;页面元素第一次绘制完成,对应指标就是首次有效绘制。

图层合成:绘制阶段可以把页面拆分为多个独立图层。当页面不同图层相互重叠,浏览器执行图层合成,保证图层按照正确顺序渲染输出到屏幕。

备注:图层可以带来动画性能收益,但是会消耗额外内存,不可以无节制滥用。

交互

页面完成绘制,并不代表页面已经可以正常交互。

比如:如果JS文件体积很大,网络速度慢,页面虽然可以看到内容,但是脚本还没有下载执行完毕,此时页面滚动、点击操作会无法响应。

重流和重绘

渲染树转换为页面布局的过程称为布局(layout);布局信息输出为屏幕像素的过程称为绘制。回流、重绘都会消耗 CPU 计算资源,会对页面性能带来阻塞影响。

脚本修改DOM、修改CSS样式,或是用户交互行为(hover、窗口缩放、页面滚动),都有可能触发回流或者重绘。

重流(回流 reflow)

当渲染树当中部分或者全部元素尺寸、位置、结构发生变化,浏览器重新计算页面布局,这个过程就是重流。

会触发回流的常见操作:

  • 页面首次渲染;
  • 浏览器窗口大小发生改变;
  • 元素尺寸、位置发生变化;
  • 元素内部文本内容、图片资源大小改变;
  • 元素字体大小发生变更;
  • DOM树上新增、删除可见元素;
  • 触发CSS伪类,例如:hover
  • 读取部分布局相关属性、调用对应API。

会触发回流的属性与 API:

clientWidth
clientHeight
clientTop
clientLeft

offsetWidth
offsetHeight
offsetTop
offsetLeft

scrollWidth
scrollHeight
scrollTop
scrollLeft

scrollIntoView()
scrollIntoViewIfNeeded()

getComputedStyle()
getBoundingClientRect()
scrollTo()

注意clientWidthoffsetWidthscrollWidthgetBoundingClientRect 等 API 使用时,浏览器发现内存里的布局缓存已经是脏的,会立刻同步执行布局计算,拿到最新准确值,这就是强制同步布局。这种情况只触发重流,不会触发重绘,也不会触发图层合成。

会触发回流的CSS属性:

width
height
padding
border
margin
position
top
left
bottom
right
float
clear
text-align
vertical-align
line-height
font-weight
font-size
font-family
overflow
white-space

重绘 (Repaint)

仅仅修改元素的样式,但是不会改变元素在文档流当中的几何位置,浏览器赋予元素新样式,重新绘制像素,这个过程就是重绘。

容易触发重绘的CSS属性:

color
border-style
border-radius
text-decoration
box-shadow
outline
background

注意:「回流一定会触发重绘」是早期浏览器没有分层的旧结论。现代浏览器具备图层分层机制:回流完成几何计算后,如果图层内部像素绘制内容没有发生变化,可以跳过重绘阶段,直接交给合成线程完成 GPU 合成。所以说,重绘不一定回流,回流也不一定重绘;回流、重绘、合成三者相互独立。

如何减少重绘与重流

浏览器自身优化机制:浏览器为了降低回流带来的性能开销,会把多次回流操作加入队列,等到合适时机批量执行,以此减少计算次数。但是当读取上面列举的布局相关属性与API的时候,浏览器必须立刻拿到最新布局结果,就会强制清空队列,立刻执行回流。尽量避免循环内频繁读取布局属性,如果需要多次使用,建议读取一次之后把结果存入变量做缓存。

CSS层面优化

  • 尽量避免滥用table布局,微小改动就可能触发整张表格重新布局;
  • 在合适场景优先使用visibility代替display: none;visibility仅触发重绘,display:none会直接改变布局,触发回流;
  • 尽量在DOM树末端修改class,缩小回流影响的节点范围;
  • 不要大量设置元素内联style样式;
  • 动画元素设置 absolutefixed 脱离普通文档流,减少对其他节点布局的影响;
  • 不推荐使用 CSS 表达式;
  • 做位移动画优先使用 transform 而不是修改 top、left;
  • 对于频繁做动画的节点,可以提升为独立合成图层。例如使用 will-change
  • CSS 硬件加速:transformopacityfilterswill-change 可以走 GPU 合成层,不会触发回流重绘;注意,修改 background-color 这类属性依旧会触发重绘。

JavaScript 层面优化

  • 不要频繁修改元素style,优先修改class,一次性变更样式;
  • 减少频繁DOM操作:可以使用documentFragment完成所有DOM操作,最后一次性插入真实DOM;也可以先设置display:none,完成DOM修改之后再恢复显示,隐藏状态下DOM操作不会触发页面回流;
  • 不要循环反复读取会触发回流的布局属性,读取结果做变量缓存;
  • 复杂动画元素使用绝对定位脱离文档流,避免父元素和兄弟节点频繁重排。

JavaScript脚本加载

JavaScript是一门轻量、支持即时编译的函数式优先脚本语言。

最初用于浏览器网页脚本,现在也可以运行在Node.js、Adobe Acrobat等非浏览器环境。

JavaScript 加载流程

浏览器正常解析HTML文档,遇到script标签,就把主线程控制权交给JS引擎。

如果是外部脚本,先下载脚本资源,下载完成之后执行代码;内联脚本直接执行。脚本执行完毕,控制权交还给渲染引擎,HTML解析继续进行。

浏览器可以并行下载多个 JS 文件,但是脚本执行顺序严格按照 HTML 当中出现的顺序,保障脚本之间的依赖关系。

浏览器对同一个域名下的并发 TCP 连接数量有限制,一般同时 6-20 个请求,避免对服务器造成压力;静态资源部署到不同域名,可以突破这个并发限制,提升加载速度。

defer属性

属性行为:

  1. HTML继续解析,同时后台并行下载外部JS脚本;
  2. HTML完整解析完毕之后,才执行defer脚本;
  3. 脚本执行顺序和标签书写顺序保持一致;
  4. DOMContentLoaded事件触发之前执行完成;

限制:defer对内联脚本、JS动态生成插入的script标签不生效。

async属性

属性行为:

  1. HTML继续解析,后台并行下载外部JS;
  2. JS脚本一旦下载完成,立刻暂停HTML解析,执行脚本;脚本执行完毕再恢复解析HTML;
  3. 执行顺序不受书写顺序约束,下载完成就执行。

限制:async脚本不保证执行顺序;不建议使用document.write;同一个标签同时写async和defer,async优先级更高。

脚本的动态加载

JS代码创建script标签插入DOM,属于动态加载脚本。默认表现类似async,下载完成立刻执行,无法保证执行顺序。

想要保证顺序,可以设置async=false,也可以监听onload事件处理回调逻辑。

CSS会阻塞JS执行

JS脚本有可能读取页面样式信息,因此浏览器会做处理:Firefox必须等待脚本前面全部CSS下载解析完毕,才执行JS;WebKit内核浏览器,如果脚本内部访问样式,就会暂停脚本执行,等待CSS解析完成之后再恢复。

预加载扫描器

主线程在解析HTML、CSS的过程中,预加载扫描器会扫描文档,提前识别图片、JS、CSS等外部资源链接,提前发起网络请求,优化页面加载速度。预加载扫描器只做资源请求,不会修改DOM树。

浏览器前缀

浏览器厂商对于尚未标准化的实验CSS属性、JS API,会增加厂商前缀,方便开发者体验新特性,同时避免正式标准落地之后破坏现有业务。等到特性标准化之后,直接使用无前缀的标准属性。

CSS 前缀

  • -webkit-:Chrome、Safari、新版 Opera,所有 iOS 平台浏览器;
  • -moz-:Firefox 浏览器;
  • -o-:旧版 Opera 浏览器;
  • -ms-:IE、旧版 Edge 浏览器。
-webkit-transition: all 4s ease;
-moz-transition: all 4s ease;
-ms-transition: all 4s ease;
-o-transition: all 4s ease;
transition: all 4s ease;

API 前缀

  • 大写接口前缀:Webkit、Moz、O、Ms;
  • 小写属性、方法前缀:webkit、moz、o、ms。
const requestAnimationFrame = window.requestAnimationFrame || 
      window.mozRequestAnimationFrame || 
      window.webkitRequestAnimationFrame || 
      window.oRequestAnimationFrame || 
      window.msRequestAnimationFrame;

扩展知识

为什么不能使用 TCP 两次握手?

如果只有两次握手:服务端收到客户端 SYN 就直接建立连接。网络中可能存在网络延迟滞留的旧 SYN 报文,当迟到的过期 SYN 到达服务端,服务端会直接创建连接;但原始客户端早已丢弃该请求,不会发送任何业务数据。于是服务端留存大量无效半连接,消耗服务器内存与资源。

三次握手机制:服务端必须收到客户端返回的第三次 ACK,才算真正完成连接建立,可以过滤这类过期无效连接。

TCP 四次挥手(释放连接)

TCP 是全双工协议,支持半关闭:一方发送 FIN,仅代表本方不再发送数据,但仍然可以接收对端发来的数据;两个方向的发送通道需要独立关闭,因此需要四次交互。

释放连接:

  • 第一次挥手:A → B,发送FIN报文,表示 A 不再发送新数据。
  • 第二次挥手:B → A,回复ACK报文,确认收到关闭请求。此时进入半关闭状态,B 依旧可以向 A 发送剩余未发完的数据。
  • 第三次挥手:B → A,发送FIN报文,表示 B 也不再发送数据。
  • 第四次挥手:A → B,回复ACK确认报文。

A 发送完 ACK 之后进入 TIME-WAIT 状态,必须等待 2MSL 时长。

注意A 进入 TIME-WAIT 状态,等待 2MSL(2 × MSL),目的是保证服务端收到最后的 ACK,防止残留旧报文干扰下一次连接。

注意:当 B 没有待发数据时,常把第 2、3 步合并为一次 FIN+ACK,语义仍是两端各自关闭发送方向。

渲染页面时常见哪些不良现象

  • FOUC:无样式内容闪烁。Firefox 浏览器会在 CSS 加载完成前渲染裸 HTML 文档,样式加载完成后页面样式突变,产生闪烁;常见原因是 CSS 加载过慢,或 CSS 放置在 HTML 文档底部。
  • 白屏现象。Chrome 类浏览器需要 DOM 树和 CSSOM 树全部构建完成才开始渲染;如果 CSS 放在文档尾部,CSS 未加载完成页面不会渲染,出现白屏;另外头部大体积 JS 阻塞 HTML 解析,也会造成白屏。

javascript: 协议

javascript:是一种特殊伪协议,把URL后面的内容当作JS代码执行。

执行之后,如果代码返回字符串,部分浏览器会使用这个字符串替换当前页面文档;如果返回undefined,页面不会发生替换。

<a href="javascript:new Date().toLocaleTimeString();">查看时间</a>
<a href="javascript:;">查看时间</a>
<a href="javascript:void(alert('测试'));">测试弹窗</a>

bookmarklet(书签小程序)javascript: 的一个典型用途。将 javascript: 开头的 JS 代码保存为浏览器书签,点击书签即可在当前页面环境执行脚本。

操作步骤

  • 打开书签栏:Ctrl+Shift+B(Windows/Linux),Mac:Cmd+Shift+B
  • 在书签栏空白位置右键 → 添加网页;
  • 输入网页名称,比如:书签小程序,再在网址输入框写入完整的 javascript: 伪协议代码;
  • 点击“书签小程序”,JS 代码立即执行。

参考链接

MDN 渲染页面:浏览器的工作原理

MDN 浏览器引擎前缀

© lizhao all right reserved,powered by Gitbook文件修订时间: 2026-08-21 23:34:33

results matching ""

    No results matching ""