本文目录

[[toc]]

预加载

  • preload 声明当前页面必定需要的资源,强制浏览器以高优先级立即异步加载
    • 浏览器关闭后立即停止加载
    • 优先级按照 astype 确定,如果是 style 将获得最高优先级( Highest ),如果是 script 可能为中( Medium )或者低( Low )
  • prefetch 提示浏览器在空闲时预加载可能在未来页面使用的资源(如下一页的脚本)
    • 即使浏览器关闭了还是会继续加载
    • 优先级最低( Lowest )
  • modulepreload :preload 基本相同,但是加载完成会解析 JS ,并下载其依赖的其他资源(如其他 JS ), vite 中就使用此功能,在 preload 中一次性加载所有依赖包,避免网络折返导致并行加载退化为串行加载

script

默认为同步加载同步解析,会阻塞 UI 渲染、交互等。

可以通过为 script 标签添加属性,修改 script 默认行为:

  • async 开启子线程异步请求,请求回来立即阻塞 HTML 解析,开始解析 JS
  • defer 开启子线程异步请求, HTML 解析完成才解析该 JS
  • type="module" 默认异步加载,加载后会视为 esm 模块进行解析、执行,能够识别 importexport 等语句,其中 import 会从 importmap 中查找包对应的 url ,支持指定同名包不同版本,支持为同一包的同一版本指定多个 url
  • type="nomodule"type="module" 脚本失效时,作为兜底使用的脚本

浏览器存储

cookie

一般存储与服务端通信的信息,比如用户 ID 等。

为了安全考虑,一般只能由服务器设置,也就是设置为 HttpOnly

在 cookie 跨站的时候,比如内嵌了 iframe 页面,需要进行跨域传递 cookie 的情况,可以设置 cookie 为 SameSite

SessionStorage 、 LocalStorage 、 IndexedDB

存储类型有效期值类型同步 / 异步容量适用场景
SessionStorage会话期间(关闭标签页即清除)字符串同步5-10MB
  • 短暂缓存的信息
LocalStorage永久(除非手动清除)字符串同步5-10MB
  • 缓存静态文件内容 JS/CSS
  • 缓存不常变更的 API 接口数据
  • 记录用户操作进度
IndexedDB永久(除非手动清除)任意类型异步50MB+
  • 大量结构化数据
  • 丢失网络连接时暂存用户操作
  • 很少修改但经常访问的后端数据

加载事件

  • DOMContentLoaded: 当 HTML 文档完全解析,且所有延迟脚本( <script defer src="…"><script type="module"> )下载和执行完毕后,会触发 DOMContentLoaded 事件。 它 不会等待样式表、图片、子框架和异步脚本 等其他内容完成加载。
  • load: 当 HTML 文档完全解析,且所有延迟脚本( <script defer src="…"><script type="module"> )下载和执行完毕后,会触发 load 事件。 它 会等待样式表,但不会等待图片、子框架和异步脚本 等其他内容完成加载。

资源加载优先级

各种资源默认优先级

资源类型highesthighmediumlowlowest
主要资源主要资源
CSSEarly CSSLate CSS媒体查询中没有匹配的 CSS 资源
JavaScriptEarly Javascript / 非 preloadJavaScriptLate JavaScriptasync JavaScript
字体普通字体preload 字体
导入资源使用 import 导入的资源,如 ESM + importmap
图片viewport 中的图片自动提升为 high前 5 张大图片 ( 超过 10000px 的图片 ) 自动提升为 medium图片默认都在这里
媒体资源视频、音频
预加载prefetch
XSL ( XML 样式表 )XSL
XHR同步 XHR异步 XHR / fetch

部分书语解释:

  • Early: 在请求任何 preload 图片之前 ,发出了请求
  • Late: 在请求任何 preload 图片之后 ,发出了请求

手动指定资源优先级

HTML 标签添加 fetchpriority 属性可以强制指定资源优先级 ,但是只能设置高 / 低 / 自动

  • high: 高优先级,在高优先级中排队加载
  • low: 低优先级,在低优先级中排队加载
  • auto: 默认值

``fetch中可以使用priority替代标签的fetchpriority属性

<!-- 不希望这个图片太早加载,虽然它是在 viewpoint 中,默认为 high 优先级 -->
<img src="/images/in_viewport_but_not_important.jpg" fetchpriority="low" alt="I'm an unimportant image!" />

<!-- 希望使用预加载,但是不希望使用太高优先级 -->
<link rel="preload" href="/js/script.js" as="script" fetchpriority="low" />

<script>
  // 不重要的请求,指定使用低优先级
  fetch('https://me.he110.site/', { priority: 'low' }).then((data) => {
    // Trigger a low priority fetch
  })
</script>

参考资料

页面生命周期

参考资料:

---
title: Chrome 页面生命周期、事件、状态循环图
---
graph TB
  Start[用户访问新页面]
  A[浏览器加载资源]

  %% 分支判断
  JPageState@{ shape: diamond, label: "document.visibilityState" }
  JPersisted@{ shape: diamond, label: "event.persisted" }

  %% 可靠 DOM 事件
  RELoad[load 事件]:::ReliableEvent
  REPageShow[pageshow 事件]:::ReliableEvent
  REPageHide[pagehide 事件]:::ReliableEvent
  REVisibilityChange[visibilitychange 事件]:::ReliableEvent
  REVisibilityChangeUncache[visibilitychange 事件]:::ReliableEvent
  REResume[resume 事件]:::ReliableEvent
  REResumeCache[resume 事件]:::ReliableEvent

  %% 不可靠 DOM 事件
  UEBeforeUnload[beforeunload 事件]:::UnreliableEvent
  UEBeforeUnloadBack[beforeunload 事件]:::UnreliableEvent
  UEUnloadBack[unload 事件]:::UnreliableEvent
  UEUnloadUncache[unload 事件]:::UnreliableEvent
  UEPageHide[pagehide 事件]

  %% 用户触发状态
  USActive[用户操作触发 ACTIVE 状态]:::UserTriggerState
  USPassive[用户操作触发 PASSIVE 状态]:::UserTriggerState
  USHidden[用户操作触发 HIDDEN 状态]:::UserTriggerState
  USHiddenUncache[用户操作触发 HIDDEN 状态]:::UserTriggerState
  USTerminated[用户操作触发 TERMINATED 状态]:::UserTriggerState

  %% 浏览器触发状态
  BSFrozen[浏览器触发 FROZEN 状态]:::BrowserTriggerState
  BSDiscarded[浏览器触发 DISCARDED 状态]:::BrowserTriggerState

  Start --> A
  A --> RELoad
  RELoad --> REPageShow

  REPageShow -->  JPageState

  JPageState --> USActive
  JPageState --> USPassive
  JPageState --> USHidden

  %% 页面可能展示的不同状态
  subgraph TD 网页所处的不同状态
    USActive -->|用户聚焦另一个 window| REBlur
    REBlur --> USPassive
    USPassive -->|用户重新聚焦这个页面| REFocus
    REFocus --> USActive
    USPassive -->|用户切换标签页,离开当前页面| REVisibleChange
    REVisibleChange --> USHidden
    USHidden -->|用户切换标签页,重新回到当前页面| REVisibleChange
    REVisibleChange --> USPassive

    USPassive -->|用户导航到新页面<br>用户关闭当前标签| UEBeforeUnload
    UEBeforeUnload --> REPageHide

    USHidden -."浏览器休眠当前页面<br>节省 CPU 占用".-> REFreeze
    REFreeze -.-> BSFrozen
    BSFrozen -->|用户切换标签页激活被冻结的页面| REResume
    REResume --> USHidden
    BSFrozen -.-> BSDiscarded
    USHidden -.内存占用过多,自动废弃页面.-> BSDiscarded
  end

  BSDiscarded -.用户切换标签回到废弃页面.-> A
  BSFrozen -->|用户通过前进/后退到当前页面<br>激活往返缓存| REResumeCache
  REResumeCache --> REPageShow
  REPageHide --> JPersisted
  JPersisted -->|页面满足往返缓存条件| REVisibilityChange
  JPersisted -->|不能缓存| REVisibilityChangeUncache
  REVisibilityChangeUncache --> USHiddenUncache
  USHiddenUncache --> UEUnloadUncache
  UEUnloadUncache --> USTerminated

  USHidden -->|用户关闭后台标签页| UEBeforeUnloadBack
  UEBeforeUnloadBack --> UEPageHide
  UEPageHide --> UEUnloadBack
  UEUnloadBack --> USTerminated

  classDef ReliableEvent fill:#FDF2D0,stroke:#2F3540;
  classDef UnreliableEvent fill:#F2CDA2,stroke:#2F3540;
  classDef UserTriggerState fill:#D3E1F1,stroke:#2F3540;
  classDef BrowserTriggerState fill:#D8D2E7,stroke:#2F3540;

浏览器多进程架构

线程 VS 进程

  • 多线程可以并行处理任务,但是线程是不能单独存在的,它是由进程来启动和管理的 一个进程就是一个程序的运行实例
  1. 进程中的任意一线程执行出错,都会导致整个进程的崩溃
  2. 线程之间共享进程中的数据
  3. 当一个进程关闭之后,操作系统会回收进程所占用的内存
  4. 进程之间的内容相互隔离

单进程浏览器时代

单进程浏览器是指浏览器的所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件、 JavaScript 运行环境、渲染引擎和页面等。其实早在 2007 年之前,市面上浏览器都是单进程的;如此多的功能模块运行在一个进程里,是导致单进程浏览器不稳定、不流畅和不安全的一个主要因素

问题 1: 不稳定

  1. 早期浏览器需要借助于插件来实现诸如 Web 视频、Web 游戏等各种强大的功能,但是插件是最容易出问题的模块,并且还运行在浏览器进程之中,所以一个插件的意外崩溃会引起整个浏览器的崩溃
  2. 渲染引擎模块也是不稳定的,通常一些复杂的 JavaScript 代码就有可能引起渲染引擎模块的崩溃

问题 2: 不流畅

同一时刻只能有一个模块可以执行,如果一个模块脚本是无限循环的,所以当其执行时,它会独占整个线程,这样导致其他运行在该线程中的模块就没有机会被执行。因为浏览器中所有的页面都运行在该线程中,所以这些页面都没有机会去执行任务,这样就会导致整个浏览器失去响应,变卡顿

问题 3: 不安全

插件可以使用 C/C++ 等代码编写,通过插件可以获取到操作系统的任意资源,当你在页面运行一个插件时也就意味着这个插件能完全操作你的电脑。如果是个恶意插件,那么它就可以释放病毒、窃取你的账号密码,引发安全性问题

浏览器多进程架构

参考 Chromium 官方发布的 架构图

  • 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下, Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
  • GPU 进程。其实, Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、 Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。
  • 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
  • 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

总结

为什么仅仅打开了 1 个页面,为什么有 4 个进程?因为打开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个;如果打开的页面有运行插件的话,还需要再加上 1 个插件进程`

为什么很多站点第二次打开速度会很快

耗时的网络请求结果被缓存,如:

  • DNS 解析(浏览器缓存 + 操作系统缓存)
  • 页面使用的资源(强缓存、协商缓存)

浏览器缓存

浏览器加载资源流程

  • 浏览器在加载资源时,根据请求头的 expires 和 cache-control( max-age 、 no-cache 等配置信息 ) 判断是否命中强缓存,是则直接从缓存读取资源,不会发请求到服务器。
  • 如果没有命中强缓存,浏览器一定会发送一个请求到服务器,通过 last-modified 和 etag 验证资源是否命中协商缓存,如果命中,服务器会将这个请求返回,但是不会返回这个资源的数据,依然是从缓存中读取资源
  • 如果前面两者都没有命中,直接从服务器加载资源

强缓存

Expires

Expires 是 HTTP/1.0 提出的一个表示资源过期时间的 Header ,它描述的是一个绝对时间,由服务器返回。

Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效。

Cache-Control

Cache-Control 出现于 HTTP/1.1 ,优先级高于 Expires ,表示的是相对时间。

  • no-store: 数据不缓存到本地,校验时永远过期
  • no-cache: 数据缓存到本地,但是校验过期时永远过期
  • public: 路径中所有主机都允许缓存,包括请求端(一般指浏览器)与 CDN 等
  • private: 只能被请求端(一般指浏览器)缓存,不允许中继节点( CDN )缓存
  • max-age: 资源过期的相对时间,值为时间戳,单位为秒,如果值不是正整数,等同于值为 0 ,即立即过期
  • s-maxage: 非 private 缓存有效,优先级高于 max-age

协商缓存

当浏览器对某个资源的请求没有命中强缓存,就会发一个请求到服务器,验证协商缓存是否命中,如果协商缓存命中,请求响应返回的 HTTP 状态为 304 并且会显示一个 Not Modified 的字符串

协商缓存是利用的是 Last-Modified + If-Modified-SinceETag + If-None-Match 这两对Header来管理的

Last-Modified + If-Modified-Since

Last-Modified 表示本地文件最后修改日期(绝对时间),浏览器会在 request header 加上 If-Modified-Since (上次返回的 Last-Modified 的值),询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来。

但是如果在本地打开缓存文件,就会造成 Last-Modified 被修改,所以在 HTTP/1.1 出现了 ETag

ETag + If-None-Match

ETag 就像一个指纹,资源变化都会导致 ETag 变化,跟最后修改时间没有关系, ETag 可以保证每一个资源是唯一的

If-None-Matchheader 会将上次返回的 ETag 发送给服务器(一般是未保存的编辑中的内容),询问该资源的 ETag 是否有更新,有变动就会发送新的资源回来

ETag 的优先级比 Last-Modified 更高

ETag 出现的原因

  • 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新 GET
  • 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说 1s 内修改了 N 次), If-Modified-Since 能检查到的粒度是 级的,这种修改无法判断(或者说 UNIX 记录 MTIME 只能精确到秒)
  • 某些服务器不能精确的得到文件的最后修改时间

如果是 POST 请求被缓存,发送的是 If-Match 而不是 ETag ,可以用与判断多用户协作编辑时,编辑版本是否更新

两种缓存的关系

强缓存是前提,协商缓存是强缓存过期后的协商方案,确认已过期的强缓存还能否继续使用

缓存相关状态码

  • 200: 缓存失效,直接返回新文件时
  • 200(from cache): 旧版本命中缓存提示,可能从内存或者磁盘中读取缓存
  • 200(from memory cache): 命中缓存,内存中已有对应缓存,不需要从磁盘读
  • 200(from disk cache): 命中缓存,但是内存中的缓存已过期,从磁盘中重新读取缓存(会更新 Last-Modified
  • 304(Not Modified): 协商缓存命中,原有的本地缓存可以继续使用

浏览器打开 URL 后发生了什么

---
title: 浏览器响应 URL 流程图
---
graph TD
    subgraph 浏览器进程
        A[用户输入] --> B[处理输入]
        B --> C[开始导航]
        C --> D{需要重定向?}
        D -->|是| E[重定向]
        E --> C
        D -->|否| F[准备渲染进程]
        F --> G[确认文档被提交]
        G --> H[页面加载完成]
    end

    subgraph 网络进程
        C --> I[发起URL请求]
        I --> J[读取响应头信息]
        J --> K{状态码正常?}
        K -->|是| L[读取响应体数据]
        K -->|否| M[错误处理]
        L --> N[提交文档]
    end

    subgraph 渲染进程
        N --> O[页面解析]
        O --> P[加载子资源]
        P --> Q[构建DOM树]
        Q --> R[页面渲染完成]
    end

    H --> R
  • 首先,浏览器进程接收到用户输入的 URL 请求,浏览器进程便将该 URL 转发给网络进程
  • 然后,在网络进程中发起真正的 URL 请求
  • 接着网络进程接收到了响应头数据,便解析响应头数据,并将数据转发给浏览器进程
  • 浏览器进程接收到网络进程的响应头数据之后,发送"提交导航 (CommitNavigation)"消息到渲染进程
  • 渲染进程接收到"提交导航"的消息之后,便开始准备接收 HTML 数据,接收数据的方式是直接和网络进程建立数据管道
  • 最后渲染进程会向浏览器进程"确认提交",这是告诉浏览器进程:“已经准备好接受和解析页面数据了”
  • 浏览器进程接收到渲染进程"提交文档"的消息之后,便开始移除之前旧的文档,然后更新浏览器进程中的页面状态

用户输入

  • 当用户在地址栏中输入一个查询关键字时,地址栏会判断输入的关键字是 搜索内容, 还是 请求的 URL
  • 如果是搜索内容,地址栏会使用浏览器默认的搜索引擎,来合成新的带搜索关键字的 URL
  • 如果判断输入内容符合 URL 规则,比如输入的是 me.he110.site ,那么地址栏会根据规则,把这段内容加上协议,合成为完整的 URL https://me.he110.site
  • 当用户输入关键字并键入回车之后,这意味着当前页面即将要被替换成新的页面, 不过在这个流程继续之前,浏览器还给了当前页面一次执行 beforeunload 事件的机会beforeunload 事件允许页面在退出之前执行一些数据清理操作,还可以询问用户是否要离开当前页面,比如当前页面可能有未提交完成的表单等情况,因此用户可以通过 beforeunload 事件来取消导航,让浏览器不再执行任何后续工作

URL 请求

浏览器进程会通过进程间通信( IPC )把 URL 请求发送至网络进程,网络进程接收到 URL 请求后,会在这里发起真正的 URL 请求流程。

  • 首先,网络进程会查找本地缓存是否缓存了该资源。如果有缓存资源,那么直接返回资源给浏览器进程
  • 如果在缓存中没有查找到资源,那么直接进入网络请求流程 (这请求前的第一步是要进行 DNS 解析,以获取请求域名的服务器 IP 地址。如果请求协议是 HTTPS ,那么还需要建立 TLS 连接)
  • 利用 IP 地址和服务器建立 TCP 连接。连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息
  • 服务器接收到请求信息后,会根据请求信息生成响应数据并发给网络进程。等网络进程接收了响应行和响应头之后,就开始解析响应头的内容了
    1. 重定向: 在接收到服务器返回的响应头后,网络进程开始解析响应头,如果发现返回的状态码是 301 或者 302 ,那么说明服务器需要浏览器重定向到其他 URL 。这时网络进程会从响应头的 Location 字段里面读取重定向的地址,然后再发起新的 HTTP 或者 HTTPS 请求,一切又重头开始
    2. 响应数据类型处理: 如果 Content-Type 字段的值被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该 URL 请求的导航流程就此结束。但如果是 HTML ,那么浏览器则会继续进行导航流程。

URL 解析

检查 URL 是否合法,并提取出对应的内容

  • 协议
  • Domain/IP
  • 端口
  • path
  • query
  • hash

DNS 解析

如果存在 Domain ,则进行 DNS 解析,如果是 IP 地址则跳过本步骤

  1. 查找浏览器内部的 DNS 缓存,查看是否有缓存 IP 地址,有则直接使用
  2. 请求本地 DNS ,本地 DNS 查找操作系统 DNS 缓存是否有缓存 IP 地址,有则直接使用
  3. 解析域名
    1. 请求根域名服务器,查询对应的顶级域名服务器地址
    2. 请求顶级域名服务器,查询对应的解析域名服务器地址
  4. 将解析返回的内容缓存到 DNS 中,如果是 CNAME 解析( CDN 一般返回的是 CNAME )还需要继续查询

建立会话

根据使用的协议,建立会话连接。

如果使用 HTTP/1 、 HTTP/1.1 、 HTTP/2 等协议,并且不在长连接状态,将进行 TCP 握手。

如果使用 HTTPS ,还需要添加一次 SSL/TLS 握手

如果使用 HTTP3 ,只需要进行一次 RTT 即可

具体握手流程可以查看 Network Note 部分

发送请求

将解析出来的数据按照数据包格式封装为请求,发送给服务端,请求包括:

  • Line: 包括请求方法、请求 URL 、 HTTP 协议版本等信息,如: POST /a.php HTTP/1.1
  • Header: 请求头,一般为 key-value 形式,如: Host: localhost:8080
  • Body: 主要传输请求参数,一般 GETDELETEOPTION 无此结构, Header 与 Body 之间使用空行隔开

响应请求

服务端处理完请求后,发回一个响应,包含以下部分:

  • Line: 包括 HTTP 协议版本、状态码、消息,如: HTTP/1.1 200 OK
  • Header: 响应头,一般为 key-value 形式,如: Content-Type: text/html;charset=UTF-8
  • Content: 返回给浏览器的主要内容, Header 与 Content 之间使用空行隔开

准备渲染进程

  • 默认情况下, Chrome 会为每个页面分配一个渲染进程,也就是说,每打开一个新页面就会配套创建一个新的渲染进程 ,即: Process-per-site-instance (基于同一站点的实例分配)
  • 同一站点( same-site )情况下多个页面会同时运行在一个渲染进程中(需要有父子页面关系,如 A.he110.site 中打开 B.he110.site ,则会共享渲染进程,如果直接新建标签页输入 B.he110.site 则不共享)

["同一站点"定义为根域名(例如: he110.info )加上协议(例如: https:// 或者 http:// ),还包含了该根域名下的所有子域名和不同的端口;比如下面这三个都是属于同一站点 ]

其余支持的进程模型:

  • Process-per-tab (每标签页独立进程)​: 每个标签页无论站点是否相同,均分配独立渲染进程
  • Process-per-site (按域名分配进程): 同一根域名(如 he110.site )下的所有页面共享一个进程,无论是否由同一父页面打开
  • 跨域 iframe 独立进程: 若页面包含跨域 iframe ,浏览器会为跨域 iframe 分配独立的渲染进程,以实现安全隔离
  • 资源限制与降级策略: 在低端设备或内存不足时, Chrome 可能降级为共享进程模式(如多个标签页共享一个进程)以节省资源

提交文档

所谓提交文档,就是指浏览器进程将网络进程接收到的 HTML 数据提交给渲染进程,具体流程是这样的:

  • 首先当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起"提交文档"的消息;
  • 渲染进程接收到"提交文档"的消息后,会和网络进程建立传输数据的"管道";
  • 等文档数据传输完成之后,渲染进程会返回"确认提交"的消息给浏览器进程;
  • 浏览器进程在收到"确认提交"的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面

这也就解释了为什么在浏览器的地址栏里面输入了一个地址后,之前的页面没有立马消失,而是要加载一会儿才会更新页面

渲染阶段

资源解析

当浏览器接收到服务器响应的资源后,首先会对资源进行解析:

  • 查看响应头的信息,根据不同的指示做对应处理,比如重定向,存储 cookie ,解压 gzip / br ,缓存资源等等
  • 查看响应头的 Content-Type 的值,根据不同的资源类型采用不同的解析方式

渲染过程

关于页面的渲染过程如下:

  1. 解析与构建阶段
    1. 解析 HTML: 生成 DOM 树,遇到 <script> 标签时可能阻塞解析。现代浏览器会预加载CSS/JS资源,并通过async/defer属性控制脚本执行顺序。
    2. ​解析 CSS: 生成 CSSOM 树,标准化属性值(如 empx颜色名RGB )。
  2. 布局阶段
    1. 构建渲染树 ( Render Tree ): 合并 DOMCSSOM ,仅包含可见元素​(如忽略 display: none 节点)。
    2. 布局计算 ( Layout / Reflow ): 生成布局树( Layout Tree ),精确计算元素几何位置和尺寸。若修改几何属性(如宽高、定位),会触发全局或局部重排。
  3. ​分层与绘制阶段
    1. 分层( Layer Tree ): 根据层叠上下文、 will-change 等属性,将元素提升为独立图层(如使用 transformopacity 的动画元素)。
    2. 生成绘制列表: 记录每个图层的绘制指令(如文本、边框、阴影),由主线程提交给合成线程。
  4. ​光栅化与合成阶段
    1. 分块 ( Tiling ): 合成线程将图层划分为图块( Tile )​,优先处理视口附近的图块。
    2. 光栅化 ( Rasterization ): 在 GPU 加速的光栅化线程池中将图块转换为位图,存储于 GPU 内存。
    3. 合成显示 ( Composite ): 合成线程通过 DrawQuad 命令将位图提交给浏览器进程,由 GPU 合成最终图像显示
---
title: 浏览器渲染页面流程图
---
graph TD
    classDef process fill:#E1F5FE,stroke:#039BE5;
    classDef data fill:#F0F4C3,stroke:#AFB42B;
    classDef merge fill:#FFCCBC,stroke:#BF360C;
    classDef repaint fill:#FFE0B2,stroke:#EF6C00;
    classDef reflow fill:#C8E6C9,stroke:#2E7D32;

    class reflow,reflow_label reflow;
    class repaint,repaint_label repaint;

    A[HTML解析]:::process --> B(DOM树):::data
    C[CSS解析]:::process --> D(CSSOM树):::data
    B --> E{合并}:::merge
    D --> E
    E --> F[渲染树]:::data

    F --> G[布局计算]:::process
    G --> H[布局树]:::data

    H --> I[分层]:::process
    I --> J[图层树]:::data

    J --> K[绘制列表]:::process
    K --> L[分块]:::process
    L --> M[光栅化]:::process

    M --> N[合成显示(GPU)]:::process

针对渲染的优化

  1. 分层与 GPU 优化

使用 transform / opacity 触发合成阶段,跳过布局和绘制,直接由合成线程处理。通过 will-change: transform 主动提示浏览器分层。

  1. 减少重排与重绘

    • ​重排触发条件: 几何属性修改(如宽度、定位)、 DOM 结构变化。
    • ​重绘触发条件: 非几何样式修改(如颜色、阴影)。
    • ​优化策略: 使用 class 批量修改样式、分离读写操作(利用浏览器渲染队列)。
  2. ​合成性能优势

合成阶段在非主线程执行,避免阻塞 JS 任务,适合高频动画场景。

回流与重绘

回流( reflow )是指: 有元素在文档流中的位置发生变化 ,比如脱离文档流、大小、几何属性等等的变化, 或者查询节点的几何属性时 (为了获得精确数据,会触发回流),就会导致的 文档流重新渲染

重绘( repaint )是指: 由于元素的 非几何属性的外观发生变化时 ,针对这一节点进行 重新绘制 的行为。

回流一定会触发重绘,重绘不一定会伴随着回流。

由于回流与重绘的代价比较高,为了提高行,浏览器会维护一个队列,存储会触发回流/重绘的操作。

等到了队列容量满,或者存储了一定时间,会清空队列并执行 回流 / 重绘 ,也就是节流操作。

当脚本尝试获取元素的准确几何信息时,会立即触发 回流 / 重绘 ,确保脚本获取到的是精确的数据。

常见浏览器内核

  • IE:trident
  • FireFox:gecko
  • Safari:webkit
  • Opera:以前是 presto,现在是 Blink
  • Chrome:Blink( 基于 webkit,Google 与 Opera Software 共同开发 )

浏览器内核的理解

主要分为渲染引擎( layout engineer 或 Rendering Engine )和 JS 引擎

  • 渲染引擎:负责取得网页的内容( HTML、XML、图像等 )、整理信息( 引入 CSS )、计算网页布局,然后输出至显示器或打印机。
  • JS 引擎:解析和执行 JavaScript 来实现网页交互

分区缓存

产生原因

原有浏览器缓存使用 协议 + 域名 + 端口 + 资源路径 作为键名,如 https://www.a.com:8080/pic.png

网站响应 HTTP 请求所需的时间可以表明浏览器过去访问过同一资源,这会使浏览器遭受安全和隐私攻击,例如:

  • 检测用户是否访问过特定网站:攻击者可以检测用户的浏览记录,方法是检查缓存中是否具有可能特定于特定网站或网站同类群组的资源。
  • 跨网站搜索攻击:攻击者可以通过检查特定网站使用的"无搜索结果"图片是否在浏览器的缓存中来检测用户搜索结果中是否包含任意字符串。
  • 跨网站跟踪:缓存可用于将类似 Cookie 的标识符存储为跨网站跟踪机制。

起始版本

  • Chrome 86
  • Safari 12.1
  • Firefox 70

具体行为

定义

  • top: 指网页 top window ,主要适配有 iframe 场景
  • current: 指发出资源请求的脚本所在的 window ,如果没有 iframe 该值为 top
  • URI: 请求资源的完整路径,格式为 协议 + 域名 + 端口 + 资源路径 作为键名,如 https://www.a.com:8080/pic.png

缓存条件

在映射缓存 key 时,修改为: top 标识 + current 标识 + URI

  • 标识的命名规则: 使用 协议 + 顶级域名 作为标识,如 https://www.a.com:8080/index.html ,会标记为 https://a.com ,忽略子域名、端口、资源路径

边界场景

  • 没有 current 的时候, current window 标识与 top window 标识保持一致
  • 发出资源请求 多次嵌套,只考虑 top + current + URI 组合是否匹配,中间层的 window 会被忽略

浏览器差异

  • Chrome: 使用 top + current + URI
  • Safari: 使用 top + URI
  • Firefox: 目前采用 Safari 方案,计划调整为 Chrome 方案

参考资料