原文链接:https://x.com/addyosmani/status/2068394292796871019
作者:Addy Osmani
Web 开发者通常将浏览器视作一个“黑盒”——它仿佛会施展魔法,将 HTML、CSS 和 JavaScript 转化为交互式的 Web 应用。然而事实是,像 Chrome (Chromium)、Firefox (Gecko) 或 Safari (WebKit) 这样的现代 Web 浏览器,是极其复杂的软件工程杰作。它统筹协调网络请求、解析并执行代码、利用 GPU 加速渲染图形,并通过沙盒进程隔离内容以保障安全。
本文将深入探讨现代浏览器的工作原理——重点聚焦于 Chromium 的架构与内核,同时也会指出其他浏览器引擎的差异之处。我们将探索从网络栈、解析流水线,到基于 Blink 的渲染流程、基于 V8 的 JavaScript 引擎、模块加载机制、多进程架构、安全沙盒,以及开发者工具的方方面面。我们的目标是为开发者提供一份易于理解的指南,揭开浏览器幕后运作的神秘面纱。
让我们开始这场探索浏览器内核的旅程吧。
每一次页面加载,都始于浏览器网络栈从 Web 获取资源。当你在地址栏输入 URL 或点击链接时,浏览器的 UI 线程(运行在“浏览器进程”中)就会发起一次导航请求。
**浏览器进程(Browser Process)**是主控进程,负责管理所有其他进程以及浏览器的用户界面。特定网页标签页之外发生的一切,都由浏览器进程控制。
主要步骤包括:
Content-Type 响应头或不正确,浏览器可能需要对 MIME 类型进行嗅探(Sniffing),以决定如何处理内容。例如,如果响应看起来像 HTML 但未标记为 HTML,浏览器依然会尝试将其作为 HTML 处理(依据宽容的 Web 标准)。这里同样存在安全措施:网络层会检查 Content-Type,并可能拦截可疑的 MIME 错配或不允许的跨源数据(Chrome 的 CORB 即“跨源读取阻塞”就是这种机制)。浏览器还会向“安全浏览(Safe Browsing)”或类似服务发起查询,以拦截已知的恶意负载。Location 头),网络代码将(在通知 UI 线程后)跟随重定向,并向新 URL 重新发起请求。只有获取到包含实际内容的最终响应时,浏览器才会进入内容处理阶段。所有这些步骤都发生在网络栈中。在 Chromium 中,网络栈运行在专门的**网络服务(Network Service)**中(作为 Chrome “服务化”改造的一部分,它现在通常是一个独立的进程)。浏览器进程的网络线程在底层利用操作系统的网络 API 协调 Socket 通信的底层工作。重要的是,这种设计意味着渲染器进程(负责执行页面代码)无法直接访问网络——它必须请求浏览器进程代为获取资源,这是一个巨大的安全优势。
现代浏览器在网络阶段实现了极其复杂的性能优化:
预加载扫描器 (Preload Scanner): Chromium 实现了一个精密的预加载扫描器,它会在主解析器之前对 HTML 标记进行分词(Tokenize)。当主 HTML 解析器被 CSS 或同步 JavaScript 阻塞时,预加载扫描器会继续扫描原始标记,寻找可以并行获取的资源(如图片、脚本和样式表)。这种机制对现代浏览器性能至关重要,且无需开发者干预即可自动运行。需要注意的是,预加载扫描器无法发现通过 JavaScript 注入的资源,这会导致这些资源按顺序加载而非并发加载。
Early Hints (HTTP 103): Early Hints 允许服务器在生成主响应的“思考时间”内,使用 HTTP 103 状态码发送资源提示。这使得可以在服务端处理期间提前发送 preconnect(预连接)和 preload(预加载)提示,有望将最大内容绘制(LCP)时间缩短数百毫秒。Early Hints 仅适用于导航请求,支持预连接和预加载指令,但不支持 prefetch(预提取)。
Speculation Rules API(推测规则 API): 这是一项较新的 Web 标准,允许开发者定义规则,根据用户的交互模式动态预提取(prefetch)和预渲染(prerender)URL。与传统的链接预提取不同,此 API 可以预渲染整个页面(包括执行 JavaScript),从而实现近乎瞬间的加载。该 API 在 <script> 标签内或 HTTP 头中使用 JSON 语法来指定应推测加载哪些 URL。为防止滥用,Chrome 设有容量限制,并根据紧急程度设置了不同的阈值。
HTTP/2 与 HTTP/3: 大多数基于 Chromium 的浏览器和 Firefox 全面支持 HTTP/2,并且广泛支持基于 QUIC 的 HTTP/3(Chrome 对支持该协议的站点默认开启)。这些协议通过支持并发传输和降低握手开销来提升页面加载速度。从开发者的角度来看,这意味着你可能不再需要使用“雪碧图(Sprite sheets)”或“域名分片(Domain sharding)”等老旧技巧——浏览器已经能在单个连接上高效地并行获取大量小文件。
资源优先级排序: 浏览器还会对某些资源进行优先级排序。通常,HTML 和 CSS 的优先级很高(因为它们会阻塞渲染),脚本可能是中等(如果正确标记了 defer/async 则可能为高),图片通常较低。Chromium 的网络栈会分配权重,甚至可以取消或推迟某些请求,以优先满足首次渲染所需的资源。开发者可以使用 <link rel="preload"> 和 Fetch Priority (提取优先级) 来影响资源的优先级。
在网络阶段结束时,浏览器拿到了页面的初始 HTML(假设这是一次 HTML 导航)。此时,Chrome 的浏览器进程会选择一个**渲染器进程(Renderer Process)**来处理这些内容。Chrome 通常会与网络请求并行(推测性地)启动一个新的渲染器进程,以便数据到达时随时待命。这个渲染器进程是隔离的(后文探讨多进程架构时会详述),它将接管解析和渲染页面的工作。
一旦完全接收到响应(或数据流开始涌入),浏览器进程就会提交导航(Commit Navigation):它向渲染器进程发出信号,让其接收字节流并开始处理页面。就在这一刻,地址栏会更新,安全指示器(HTTPS 锁等)也会针对新站点显示。现在,舞台交给了渲染器进程:解析 HTML、加载子资源、执行脚本,并绘制页面。
当渲染器进程接收到 HTML 内容时,其主线程(Main thread)开始按照 HTML 规范对其进行解析。解析 HTML 的结果是 DOM(文档对象模型)——一棵代表页面结构的对象树。解析是增量的,并且可以与网络读取交织进行(浏览器以流式方式解析 HTML,因此甚至在整个 HTML 文件下载完毕之前,DOM 就可以开始构建)。
HTML 解析与 DOM 构建: HTML 规范将 HTML 解析定义为一个具有容错性的过程。无论标记多么不规范,解析器都能生成一个 DOM。这意味着,即使你忘记了闭合 </p> 标签或者标签嵌套错误,解析器也会隐式地修复或调整 DOM 树使其合法。例如,<p>Hello <div>World</div> 会在 DOM 结构中自动将 <p> 闭合在 <div> 之前。解析器为 HTML 中的每个标签或文本创建 DOM 元素和文本节点。每个元素都被放置在一棵反映源码嵌套关系的树中。
一个重要的细节是,HTML 解析器在运行过程中可能会遇到需要获取的资源:例如,遇到 <link rel="stylesheet" href="..."> 会促使浏览器(在网络线程上)请求 CSS 文件;遇到 <img src="..."> 会触发图片请求。这些请求与解析并行发生。解析器在加载这些资源时可以继续解析,但有一个重大例外:脚本。
处理 <script> 标签: 如果 HTML 解析器遇到一个 <script> 标签,它会暂停解析,并(在默认情况下)必须先执行完脚本才能继续。这是因为脚本可能使用 document.write() 或其他 DOM 操作 API,从而改变页面结构或后续即将加载的内容。通过在这个时间点立即执行脚本,浏览器保证了操作顺序与 HTML 的相对位置一致。因此,解析器将脚本移交给 JavaScript 引擎执行,只有当脚本执行完毕(且它所做的任何 DOM 修改生效)后,HTML 解析才能恢复。这种脚本阻塞行为解释了为什么在 <head> 中引入大型 <script> 文件会拖慢页面渲染——在脚本下载并运行完毕之前,HTML 解析根本无法继续。
不过,开发者可以通过属性来改变这种行为:在 <script> 标签上添加 defer 或 async(或使用现代的 ES Module 脚本)可以改变浏览器的处理方式:
async:脚本文件会被并行下载,一旦准备就绪便立即执行,不暂停 HTML 解析(解析器不等待,且脚本不保证按原有的相对顺序执行)。defer:脚本同样并行下载,但执行会被推迟到 HTML 解析完成之后(并将在稍后按原有的先后顺序执行)。<script type="module">)默认也是 defer 的(它们还可以使用 import 语句——后文会单独讲解模块加载)。使用这些技巧,浏览器可以在没有长停顿的情况下继续构建 DOM,从而显著提升页面加载速度。CSS 解析与 CSSOM: 与 HTML 类似,CSS 文本也必须被解析成浏览器能够操作的结构——通常称为 CSSOM(CSS 对象模型)。CSSOM 本质上是应用于文档的所有样式(规则、选择器、属性)的内部表示。浏览器的 CSS 解析器读取 CSS 文件(或 <style> 块),将它们转化为 CSS 规则列表(内部使用了大量的布隆过滤器等机制来加速样式解析)。接着,在构建 DOM 的同时(或当 DOM 和 CSSOM 都就绪后),浏览器会为每个 DOM 节点计算样式。这一步通常被称为样式解析(Style Resolution)或样式计算(Style Calculation)。浏览器将 DOM 和 CSSOM 结合起来,根据层叠规则、继承和默认样式,决定每个元素适用哪些 CSS 规则以及最终的“计算样式(Computed styles)”。其输出可以被理解为每个 DOM 节点与其计算样式(元素最终解析出的 CSS 属性,如颜色、字体、大小等)的映射。
值得注意的是,即使没有任何开发者编写的 CSS,每个元素都有浏览器的默认样式(User-Agent 样式表)。例如,<h1> 在几乎所有浏览器中都有默认的 font-size 和 margin。浏览器内置样式规则以最低优先级应用,确保页面有合理的默认呈现。开发者可以在 DevTools 的“Computed (计算样式)”面板查看元素最终应用了哪些确切的 CSS 属性。
渲染阻塞行为: 虽然没有完全加载完 CSS 时 HTML 依然可以继续解析,但两者之间存在“渲染阻塞”关系:浏览器通常会等待 CSS 加载完毕后才进行首次渲染(针对位于 <head> 中的 CSS)。这是因为应用不完整的样式表会导致“未渲染内容闪烁”(FOUC)。在实践中,如果一个未标记 async/defer 的 <script> 出现在 CSS <link> 之后,解析器在执行该脚本前,还会额外等待 CSS 加载完成(因为脚本可能会通过 DOM API 查询样式信息)。经验法则:将样式表链接放在 <head> 中(它们会阻塞渲染,但早期就需要),将非关键或大型脚本加上 defer/async 或放在底部,以免延迟 DOM 解析。
现在浏览器已经具备了:(1) 由 HTML 构建的 DOM,(2) 解析后的 CSS 规则 CSSOM,以及 (3) 每个 DOM 节点的计算样式。这三者构成了下一个阶段——“布局(Layout)”的基础。但在进入布局之前,我们先稍作探讨 JavaScript 方面:当 JS 引擎(在 Chrome 中是 V8)执行代码时会发生什么?我们将在后续小节专门深入探讨 V8 内部机制。目前只需知道,脚本在运行时可能会修改 DOM 或 CSSOM。浏览器必须响应这些变化,按需重新计算样式或布局(如果频繁进行,会产生性能成本)。
在这个阶段,浏览器的渲染器进程已经知道了 DOM 的结构以及每个元素的计算样式。接下来的问题是:这些元素在屏幕上的确切位置在哪里?它们有多大?这就是布局(Layout)——有时也称为“回流(Reflow)”——的任务。在这个阶段,浏览器根据 CSS 规则(普通流、盒模型、Flexbox 或是 Grid 等)和 DOM 层级,计算出每个元素的几何信息(大小和位置)。
布局树的构建: 浏览器遍历 DOM 树并生成一棵布局树 (Layout Tree)(有时被称为渲染树 Render Tree 或 帧树 Frame Tree)。布局树在结构上与 DOM 树相似,但它会忽略非视觉元素(例如 <script> 或 <meta> 标签不会产生盒子),并且在需要时可能会将某些元素拆分为多个盒子(例如,分布在多行中的内联元素可能会对应多个布局盒)。布局树中的每个节点都持有该元素的计算样式,包含其内容(文本或图片)以及影响布局的计算属性(如 width、height、padding 等)。
在布局过程中,浏览器计算每个元素盒子的精确坐标(x, y)和尺寸(宽,高)。这涉及 CSS 规范中定义的复杂算法:例如,在普通文档流中,块级元素默认自上而下堆叠并占据整个宽度,而内联元素在行内流动并按需换行。Flexbox 或 Grid 等现代布局模式则有其专有的算法。引擎必须考虑字体度量以进行换行(因此文本布局涉及测量文本排版段落),还必须处理外边距(Margin)、内边距(Padding)、边框(Border)等。这里存在海量的边缘情况(如外边距折叠、浮动、脱离文档流的绝对定位元素等),使得布局成为一个惊人复杂的计算过程。即使是“简单”的自上而下布局,也必须计算依赖于可用宽度和字体大小的文本换行。浏览器引擎有专门的团队进行多年开发,以保证布局的准确与高效。
关于布局树的几个细节:
display: none 的元素会被完全从布局树中剔除(不生成任何盒子)。相反,仅仅是不可见(如 visibility: hidden)的元素仍会生成布局盒(占据空间),只是稍后不被绘制。::before 或 ::after)如果生成了内容,则会包含在布局树中(因为它们具有视觉盒子)。<p> 元素的布局节点知道它相对于视口的位置和尺寸,并拥有其内部每行或内联盒子的子节点。布局计算: 布局通常是一个递归过程。从根节点(<html> 元素)开始,浏览器计算视口(Viewport)的大小,然后在其内部布局子元素,以此类推。许多元素的尺寸依赖于它的子元素或父元素。布局算法经常需要对浮动等复杂交互进行多次遍历,但通常它是单向(自上而下)进行的,必要时会进行回溯。
这一阶段结束时,页面上每个元素的坐标和大小都已确定。我们现在可以将页面概念化为一堆内部包含文本或图片的“盒子”。但是,我们还没有真正在屏幕上画出任何东西——这就是下一步:绘制。
需要牢记的一个关键概念是:布局是一项昂贵的操作,尤其是频繁进行时。如果 JavaScript 随后改变了元素的大小或添加了内容,它可能会强制对部分或全部页面进行重新布局 (Relayout)。开发者常听到要避免**“布局抖动 (Layout Thrashing)”**的建议(例如在 JS 中修改 DOM 后立即读取布局信息,这会强制执行同步重新计算)。浏览器会尝试进行优化,标记布局树中“脏(dirty)”的部分,并仅重新计算这些部分。但在最坏的情况下,DOM 树高层的改变可能需要重新计算整个大页面的布局。这就是为什么应尽量减少昂贵的样式/布局操作以提升性能。
样式与布局总结:
从 HTML 和 CSS 出发,浏览器构建了:
每个阶段都建立在前一个阶段之上。如果任意阶段发生变化(例如脚本改变了 DOM 或修改了 CSS 属性),后续阶段都可能需要更新。这条影响链意味着布局和绘制依赖于最新的样式,以此类推。
绘制 (Painting) 是获取结构化的布局信息,并在屏幕上实际生成像素的过程。在传统的认知中,浏览器会遍历布局树并为每个节点发出绘制指令(“在这个坐标画背景,画文字,画图片”)。现代浏览器在概念上依然这么做,但通常会将工作拆分为多个阶段,并利用 GPU 来提升效率。
绘制 / 光栅化 (Rasterization): 在布局完成后的渲染器主线程上,Chrome 会通过遍历布局树生成绘制记录 (Paint records)(或称为显示列表 Display List)。这基本等同于一系列带有坐标的绘制操作指令,就像画家规划如何作画一样:“在 (x,y) 画一个宽高为 W 和 H、填充蓝色的矩形,然后在 (x2, y2) 用 XYZ 字体画‘Hello’文字……”。这个列表是按照正确的 z-index 顺序生成的,以确保重叠的元素能正确覆盖。浏览器必须处理层叠上下文 (Stacking contexts) 和透明度等,以获得正确的顺序。
过去,浏览器可能只是简单地按顺序直接在屏幕上绘制每个元素。但如果页面只有部分内容发生变化,这种做法(重绘整个页面)就会非常低效。因此,现代浏览器会将这些绘制指令记录下来,然后利用合成 (Compositing) 步骤将最终图像组装起来,并在此过程中大量应用 GPU 加速。
分层与合成 (Layering and Compositing): 合成是一种优化策略,即将页面拆分为可以独立处理的多个图层 (Layers)。例如,一个具有 CSS transform 或动画的定位元素可能会获得自己的图层。图层就像独立的“草稿画布”——浏览器可以独立对每个图层进行光栅化(将绘制指令转化为像素),然后合成器将它们在屏幕上混合在一起,这通常由 GPU 完成。
在 Chromium 的管线中,生成绘制记录后,会有一个构建图层树 (Layer tree) 的步骤。有些图层是自动创建的(如 <video> 或 <canvas> 元素,或者带有特定 CSS 属性的元素会被提升为图层),开发者也可以使用 will-change 或 transform 这样的属性来显式提示浏览器创建图层。分层的好处在于:一个图层上的移动或透明度变化可以直接被“合成”(即只需重新渲染或移动该图层),而无需重绘整个页面。然而,过多的图层会消耗大量内存并增加开销,因此浏览器对此非常谨慎。
确定图层后,Chrome 主线程将任务移交给合成器线程 (Compositor Thread)。合成器线程运行在渲染器进程中,但独立于主线程(因此即使 JS 主线程正忙,它也能继续工作,这对于实现流畅的滚动和动画至关重要)。合成器线程的工作是提取这些图层,对它们进行光栅化(将矢量绘图转换为实际的像素位图),并将它们组装成帧。
借助 GPU 的光栅化: 光栅化任务也可以并行分配。在 Chrome 中,合成器线程会将图层分割成更小的瓦片 (Tiles)(想象 256x256 或 512x512 像素的块)。然后,它将这些瓦片分派给多个光栅化工作线程(甚至可以跨多个 CPU 核心运行)并发执行。每个工作线程负责一个瓦片——本质上就是针对该区域的绘制指令——并生成位图(像素数据)。重要的是,Chrome 的图形库 Skia 可以利用 CPU 或 GPU 进行光栅化;通常这些线程在 CPU 上渲染像素,然后将其上传到 GPU 内存中作为纹理 (Textures)。当所有需要的瓦片都绘制完成后,合成器线程就准备好了一套带有纹理的图层。
合成器接着组装出一个合成器帧 (Compositor frame) —— 这本质上是一条发给浏览器进程的消息,其中包含了构成屏幕画面的所有四边形(quads/图层瓦片)、它们的位置等信息。该帧通过 IPC(进程间通信)发送回浏览器进程,并最终由浏览器的 GPU 进程(Chrome 中专用于访问 GPU 的进程)接手并在屏幕上显示。浏览器的界面(如标签栏)同样也通过合成器帧绘制,在最后一步中混合。GPU 进程接收帧,通过底层 API(OpenGL / DirectX / Metal 等)极快地处理,在屏幕正确位置绘制每个纹理并应用变换。这就是你看到的最终画面。
当你滚动页面或观看动画时,这条管线的优势展露无遗。例如,滚动页面主要只是在较大的页面纹理上改变视口 (Viewport)。合成器只需移动图层位置并请求 GPU 绘制进入视口的新部分,主线程完全无需重新绘制任何东西。如果动画只是一个变换(如移动一个独占图层的元素),合成器线程每帧都能更新该元素的位置并生成新帧,完全不需要主线程参与,也不需要重新运行样式和布局计算。这就是为什么建议使用**“仅触发合成 (compositing-only)”**的动画(如改变 transform 或 opacity,不触发布局)——即使主线程正忙,它们也能流畅地以 60 FPS 运行。相反,像动画 height 这样的属性会强制每一帧重新布局和绘制,如果主线程跟不上,就会导致严重的卡顿。
简言之,Chrome 的渲染管线是:DOM → 样式 → 布局 → 绘制(记录指令) → 分层 → 光栅化(瓦片) → 合成 (GPU)。 Firefox 使用 WebRender 的管线在前半部分类似,但它跳过了显式构建图层,而是直接向 GPU 发送显示列表,由 GPU Shader 处理几乎所有绘制操作。Safari (WebKit) 同样使用了多线程合成和基于硬件(Mac 上的 CALayer)的 GPU 渲染。
为了保证平滑渲染,对于每一个动画帧(目标是 60fps,即每帧 16.7ms),合成器都会努力产出一帧。如果主线程执行 JavaScript 花了太长时间,合成器可能会跳帧,导致肉眼可见的卡顿。开发者可以使用 requestAnimationFrame 将 JS 更新与帧边界对齐,从而辅助平滑渲染。
JavaScript 驱动了网页的交互行为。在 Chromium 中,V8 引擎负责执行 JavaScript 和 WebAssembly。
<script> 时,V8 首先解析源码生成抽象语法树 (AST)。它包含一个轻量级的“预解析器 (Preparser)”,对函数仅做最基础的合法性验证(跳过内部实现),直到函数真正被调用时才进行完整解析。V8 随后使用名为 Ignition 的解释器将 AST 转化为紧凑的字节码 (Bytecode)。字节码启动快,非常适合缩减页面初始加载时间。JavaScript 是动态类型的语言,直接解释执行效率较低。V8 拥有多层即时编译器 (JIT Compilers),它的理念是:对执行频繁的“热点”代码投入更多编译时间进行极度优化,以提升运行速度。
如果在后续执行中,变量的类型打破了原有的优化假设(例如一个一直传入数字的函数突然传入了字符串),V8 会执行**“去优化(Deoptimization,或逆优化)”**,退回到未优化的状态。因此,保持 JavaScript 变量类型的稳定性对于维持极致性能至关重要。
V8 使用被称为 Orinoco 的垃圾回收器,它是分代、增量且并发的:
对于开发者来说,现代 GC 的性能已经好到“毫无存在感”,但依然要注意避免在紧凑的循环中创建海量长生命周期的对象。此外,像 document.querySelector 或网络请求等 Web API 实际上并不是 V8 的一部分,它们是浏览器(用 C++ 编写)通过接口绑定暴露给 V8 引擎调用的。
与传统的 <script> 标签不同,ES Modules 拥有独立的加载和执行模型。
<script type="module">,浏览器会把它作为入口点。它会解析代码并找出所有的 import 语句,并递归地获取和解析所有依赖的模块,构建出一张模块依赖图 (Dependency graph)。这一过程是异步的,只有当依赖图彻底就绪后,模块才会依据依赖顺序(最底层的依赖最先执行)依次运行。import(): 可以在代码执行期间动态按需加载模块,它返回一个 Promise。这对“代码分割(Code-splitting)”极为有用。import { react } from 'react' 会报错,因为 'react' 不是有效 URL。Import Maps(<script type="importmap">)允许开发者在浏览器端配置映射表,将包名映射到实际的 CDN 网址。它已经被三大主流浏览器全面支持,是目前去打包化(unbundled)开发的基石。现代浏览器不再是“单进程”巨兽。Chromium 首创了多进程架构以换取稳定性、安全性和性能隔离。
多进程的优势:
这构成了浏览器最坚固的防线:
evil.com。在过去,它们可能在同一个渲染器进程内运行;而现在,Chrome 会将这个跨源的 iframe 拆分到一个独立的进程中。这两个进程通过极其复杂的 IPC(进程间通信)机制,在后台拼装成用户眼前的单个网页。对于开发者,这意味着跨源通信(如 postMessage)实际上跨越了进程边界,但浏览器底层优雅地屏蔽了这些复杂性。代价是,更多的进程带来了相对更高的内存占用(约 10-20%)。
Chromium (Chrome/Edge/Blink)、Gecko (Firefox) 和 WebKit (Safari) 这三大引擎在总体宏观架构上高度趋同,但技术实现上各有千秋:
了解浏览器的“黑盒”之后,作为开发者我们可以获得以下具备指导意义的心智模型:
<link rel="preload"> 和 preconnect 帮助预加载扫描器尽早开工。transform 和 opacity 做动画,它们在“合成器线程”上运行,完全不会被主线程上阻塞的 JavaScript 干扰。Web 浏览器是当代软件工程中最令人惊叹的杰作之一。它们精妙地抽象了底层操作系统的极度复杂性,让我们能用纯粹的 HTML/CSS/JS 构建丰富多彩的世界。但当你愿意掀开引擎盖,一窥底层的奥秘时,你定会写出性能更极致、更经得起考验的 Web 应用。
(注:如果想进一步深入学习,极力推荐阅读 Pavel Panchekha 和 Chris Harrelson 编写的开源书籍《Browser Engineering》(browser.engineering),以及 Google Chrome 团队的“Inside look at modern web browser”系列文章。)

扫码关注w3ctech微信公众号
共收到0条回复