多进程架构
现代浏览器(以Chrome为代表)采用多进程架构,将不同的职责分配给独立的进程,以提升稳定性、安全性和性能。主要进程包括:浏览器进程(Browser Process,负责用户界面、地址栏、书签栏、前进后退按钮、网络请求和文件访问等,是浏览器的主进程)、渲染进程(Renderer Process,每个标签页通常拥有一个独立的渲染进程,负责HTML解析、CSS计算、JavaScript执行、页面渲染,通过沙箱机制限制对系统资源的直接访问)、GPU进程(GPU Process,负责处理所有与GPU相关的任务,如CSS 3D变换、Canvas绘制、合成层的最终渲染,多个渲染进程共享同一个GPU进程)、网络进程(Network Process,负责网络请求的处理,从Chrome 83开始独立,此前由浏览器进程负责)、插件进程(Plugin Process,每个插件实例运行在独立的进程中,插件崩溃不会影响浏览器主体)和实用程序进程(Utility Process,处理特定任务如音频服务、存储服务等)。
站点隔离(Site Isolation)是Chrome 67引入的安全特性,它为每个跨站iframe分配独立的渲染进程,防止恶意网站通过Spectre等侧信道攻击窃取其他站点的数据。这进一步增加了进程数量,但显著提升了安全性。内存不足时,浏览器会合并同一站点的多个标签页到同一个渲染进程,以节省内存。
渲染进程内部是多线程架构,主要线程包括:主线程(Main Thread,负责执行JavaScript、解析HTML和CSS、计算样式、构建DOM和CSSOM树、执行布局)、合成器线程(Compositor Thread,负责接收用户输入事件、管理图层合成、将合成后的帧提交给GPU进程)、光栅化线程(Raster Thread,将绘制记录转换为位图,可以是一个或多个,由合成器线程调度)和工作线程(Worker Thread,用于执行Web Worker、Service Worker等脚本)。
解析流程
浏览器从网络进程接收到HTML文档的字节流后,主线程开始解析HTML。解析过程分为词法分析(Tokenization)和语法分析(Tree Construction)两个阶段。词法分析器(Tokenizer)将字节流转换为Token序列(如开始标签、结束标签、属性、文本内容)。语法分析器根据Token序列构建DOM树(Document Object Model),遵循HTML5规范定义的状态机算法。
HTML解析是增量式的——浏览器不需要等待整个文档下载完成才开始解析和渲染。解析器在接收到数据的同时进行解析,逐步构建DOM树并渲染到屏幕上。这种流式解析显著提升了感知性能。
当解析器遇到<script>标签时(没有defer或async属性),会暂停HTML解析,下载并执行JavaScript代码。这是因为JavaScript可能通过document.write修改DOM,改变后续的解析结果。这种阻塞行为是首屏渲染延迟的常见原因,因此通常建议将非关键的脚本放在文档底部,或使用defer/async属性。defer脚本在HTML解析完成后、DOMContentLoaded事件前按顺序执行;async脚本下载完成后立即执行,不保证执行顺序,也不阻塞DOMContentLoaded。
当解析器遇到<link rel="stylesheet">时,CSS的下载和解析通常不会阻塞HTML解析(现代浏览器的优化),但会阻塞JavaScript的执行(因为JavaScript可能需要查询样式信息),从而间接阻塞HTML解析。将CSS放在head中,使其尽早开始下载和解析,是避免渲染阻塞的最佳实践。
预解析(Pre-parsing)或预加载扫描器(Preload Scanner)是浏览器的一项优化技术。在主线程解析HTML的同时,另一个轻量级扫描器提前发现文档中的外部资源(如图片、CSS、JavaScript),发起预加载请求。这不会阻塞主线程的解析,但能够提前开始资源下载,减少等待时间。
渲染管线
浏览器的渲染管线(Rendering Pipeline)将HTML、CSS和JavaScript转换为屏幕上的像素。关键步骤包括:构建DOM树、构建CSSOM树、合并为渲染树(Render Tree)、执行布局(Layout)、执行绘制(Paint)和合成(Composite)。
DOM树(Document Object Model)是HTML文档的内存表示,每个HTML元素对应DOM树中的一个节点,包含元素的属性、文本内容和父子关系。CSSOM树(CSS Object Model)是CSS样式的内存表示,包含所有选择器、属性和规则,以及来自浏览器默认样式表(User Agent Stylesheet)的样式。CSSOM的构建是阻塞的——浏览器必须等待所有CSS(包括外部样式表和内联样式)下载和解析完成后,才能继续后续步骤。
渲染树(Render Tree)是DOM树和CSSOM树合并的产物,它只包含可见的节点(display: none的节点不会被包含,visibility: hidden的节点会被包含但标记为不可见)。渲染树的每个节点称为渲染对象(Render Object),包含节点的几何信息(位置、尺寸)和绘制信息(颜色、字体、边框等)。
布局(Layout,又称Reflow)计算渲染树中每个节点的精确位置和尺寸。布局是一个递归过程,从根节点(通常是html元素对应的渲染对象)开始,遍历渲染树,根据CSS盒模型和布局算法(如块级布局、行内布局、Flexbox、Grid)计算每个节点的布局信息。布局的输出是一个盒模型树(Box Tree),包含每个元素的精确坐标和尺寸。
绘制(Paint)将布局计算的结果转换为绘制指令(Paint Records)。绘制分为多个阶段(Paint Phases),每个阶段处理特定类型的绘制:背景、边框、内容、轮廓等。绘制指令被记录到绘制记录表(Paint Record List)中,而非立即执行。这些记录随后被提交给光栅化线程处理。
重排与重绘
重排(Reflow)是指当元素的尺寸、位置或布局属性发生变化,浏览器需要重新计算该元素及其相关元素的布局信息。触发重排的操作包括:修改元素的尺寸(width、height、margin、padding、border)、修改元素的位置(top、left、right、bottom,在定位元素中)、改变字体大小、添加或删除可见的DOM元素、修改影响布局的CSS属性(如display、position、float)、读取某些布局相关的属性(如offsetHeight、clientWidth、getBoundingClientRect,这些操作会强制浏览器立即计算布局,称为强制同步布局或Forced Synchronous Layout)。
重排的影响范围可能是局部的(仅影响当前元素及其子元素)或全局的(影响整个文档)。现代浏览器通过脏矩形系统(Dirty Rectangle System)和布局缓存优化重排性能,但频繁的重排仍然是性能瓶颈的主要来源。
重绘(Repaint)是指当元素的外观属性(如颜色、背景、边框颜色、阴影、可见性)发生变化,但布局信息未改变时,浏览器只需要重新绘制该元素的外观。重绘的代价小于重排,因为它不需要重新计算布局,但仍需要执行绘制指令和光栅化。触发重绘的操作包括:修改颜色、背景色、边框颜色、文字阴影、盒阴影、visibility(从hidden到visible,不涉及布局变化)。
优化重排和重绘的策略包括:批量修改样式(将多次样式修改合并为一次,通过添加/移除class而非逐个修改style属性);使用文档片段(DocumentFragment)或离线DOM(cloneNode + 批量修改 + replaceChild)进行批量DOM操作;避免在循环中读取布局属性(将读取和写入分离,先批量读取,再批量写入);使用transform和opacity实现动画(这些属性可以由合成器线程处理,不触发重排和重绘);使用will-change属性提示浏览器提前创建合成层(需谨慎使用,避免创建过多合成层消耗内存)。
合成与分层
合成(Compositing)是渲染管线的最后阶段,它将页面的不同部分分层,独立光栅化,然后由GPU合成为最终图像。这种分层渲染机制是现代浏览器性能优化的核心。
图层(Layer)是渲染树的一个子集,每个图层独立绘制和光栅化。创建新图层的常见条件包括:元素具有3D变换(transform: translateZ(0)等)、元素具有opacity动画、元素具有will-change: transform或will-change: opacity、元素被视频或canvas覆盖、元素使用CSS滤镜、元素具有固定或粘性定位(在某些浏览器中)、元素具有overflow-scrolling: touch(在移动浏览器中)。
光栅化(Rasterization)将绘制指令转换为位图(Bitmap)。光栅化可以在CPU上执行(软件光栅化),也可以在GPU上执行(硬件加速光栅化)。GPU光栅化通过Skia库向GPU进程发送GL命令,将绘制指令转换为纹理(Texture)。合成器线程将图层的光栅化结果(纹理)收集起来,按照正确的顺序和变换,发送给GPU进程进行最终合成。
层爆炸(Layer Explosion)是过度使用will-change或不当触发图层创建导致的性能问题。每个图层都需要额外的内存存储纹理,过多的图层会消耗大量GPU内存,甚至导致内存不足。因此,图层优化应当基于实际性能测量,而非盲目添加will-change。
事件机制
浏览器中的事件处理遵循DOM事件流(DOM Event Flow)规范,包括三个阶段:捕获阶段(Capture Phase,事件从document根节点向下传播到目标元素)、目标阶段(Target Phase,事件到达目标元素)和冒泡阶段(Bubble Phase,事件从目标元素向上传播回document根节点)。默认情况下,事件监听器在冒泡阶段触发,通过addEventListener的第三个参数设置为true可以改为捕获阶段触发。
事件委托(Event Delegation)是优化事件处理的常用技术。通过在父元素上注册事件监听器,利用事件冒泡机制处理子元素的事件,可以减少监听器数量,节省内存,并自动处理动态添加的子元素。例如,在ul元素上监听click事件,通过event.target判断实际点击的li元素,而非为每个li单独注册监听器。
事件对象的常用属性和方法包括:target(事件起源元素)、currentTarget(当前处理事件的元素,在事件委托中与target可能不同)、type(事件类型)、preventDefault()(阻止默认行为,如链接跳转、表单提交)、stopPropagation()(阻止事件继续传播,包括捕获和冒泡)、stopImmediatePropagation()(阻止事件传播且阻止同元素上的其他监听器执行)。
合成器线程负责接收用户输入事件(如滚动、触摸、鼠标移动),并将事件转发给主线程。为了提升响应性,浏览器会尝试以60fps(每16.67ms一帧)的频率处理事件和渲染。如果主线程被长任务阻塞,事件处理会被延迟,导致界面卡顿。因此,将长任务拆分为多个小任务(使用requestIdleCallback或setTimeout分段执行)是保持界面流畅的关键策略。
passive事件监听器(通过addEventListener的{ passive: true }选项设置)向浏览器声明监听器不会调用preventDefault()。这使得浏览器可以在事件监听器执行前就开始滚动或缩放等默认行为,显著提升了滚动性能。对于touchstart和touchmove事件,现代浏览器默认将监听器视为passive,除非显式设置为passive: false。
Intersection Observer API提供了一种异步观察目标元素与其祖先元素或视口交叉状态的方法。相比传统的scroll事件监听配合getBoundingClientRect计算,Intersection Observer由浏览器内部优化实现,不会导致主线程的频繁计算,非常适合实现懒加载、无限滚动和可见性统计。
Resize Observer API(比window.resize事件更精确)和Mutation Observer API(异步观察DOM变化)也是浏览器提供的性能友好的观察机制,它们将原本需要轮询或频繁同步计算的操作转为浏览器内部的异步回调,减少了主线程负担。
存储机制
浏览器提供了多种客户端存储机制,各有其容量限制、生命周期和适用场景。Cookie是最古老的存储方式,容量通常限制为4KB,每次HTTP请求都会自动携带,适合存储会话标识等小型数据,但频繁传输增加了网络开销。
localStorage和sessionStorage是Web Storage API提供的键值对存储。localStorage的数据没有过期时间,除非被显式删除,否则会一直保留,容量通常为5MB至10MB。sessionStorage的数据仅在当前页面会话期间有效,页面关闭(包括标签页或浏览器窗口)时数据清除。两者都是同步API,会阻塞主线程,且只能存储字符串(复杂对象需通过JSON.stringify序列化)。Web Storage不适合存储大量数据或需要频繁读写的场景。
IndexedDB是浏览器提供的结构化数据库系统,基于对象存储(Object Store)而非关系表。它支持索引、事务、游标和范围查询,容量通常远大于Web Storage(可达数百MB甚至更多,取决于浏览器和磁盘空间)。IndexedDB是异步API,不会阻塞主线程,适合存储大量结构化数据、离线应用数据、缓存资源等。但其API基于事件驱动,较为繁琐,通常使用封装库(如localForage、Dexie.js)简化操作。
Cache API是Service Worker规范的一部分,提供对Request/Response对象对的存储能力,专为HTTP请求的缓存设计。它使Service Worker能够拦截网络请求,从缓存中返回匹配的响应,实现离线访问和缓存优先策略。Cache API的存储容量通常与IndexedDB共享配额。
Storage API(navigator.storage)提供了存储配额和持久化状态的查询与管理。navigator.storage.estimate()返回当前源的存储使用量和配额上限。navigator.storage.persist()请求将存储标记为持久化,浏览器在清理存储时会优先保留持久化数据(但用户仍可以手动清除)。Storage Manager使Web应用能够更智能地管理本地存储资源。