渲染流程:
1.解析HTML,生成DOM树(DOM)
2.解析CSS,生成CSSOM树(CSSOM)
3.将DOM和CSSOM合并,生成渲染树(Render-Tree)
4.计算渲染树的布局(Layout)
5.将布局渲染到屏幕上(Paint)
生成渲染树
为了构建渲染树,浏览器主要完成了以下工作:
- 从DOM树的根节点开始遍历每个可见节点。
- 对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。
- 根据每个可见节点以及其对应的样式,组合生成渲染树。
第一步中,既然说到了要遍历可见的节点,那么我们得先知道,什么节点是不可见的。不可见的节点包括:
- 一些不会渲染输出的节点,比如script、meta、link等。
- 一些通过css进行隐藏的节点。比如display:none。注意,利用visibility和opacity隐藏的节点,还是会显示在渲染树上的。只有display:none的节点才不会显示在渲染树上。
从上面的例子来讲,我们可以看到span标签的样式有一个display:none,因此,它最终并没有在渲染树上。
注意:渲染树只包含可见的节点。
在解析html的过程中,html的解析会被中断,这是因为javascript会阻塞dom的解析。当解析过程中遇到script标签的时候,便会停止解析过程,转而去处理脚本,如果脚本是内联的,浏览器会先去执行这段内联的脚本,如果是外链的,那么先会去加载脚本,然后执行。在处理完脚本之后,浏览器便继续解析HTML文档。
同时javascript的执行会受到标签前面样式文件的影响。如果在标签前面有样式文件,需要样式文件加载并解析完毕后才执行脚本。这是因为javascript可以查询对象的样式。
js 阻塞了什么
因为js在执行的过程中可能会操作DOM,发生回流和重绘,所以GUI渲染线程与JS引擎线程是互斥的。
在解析HTML过程中,如果遇到 script 标签,渲染线程会暂停渲染过程,将控制权交给 JS 引擎。内联的js代码会直接执行,如果是js外部文件,则要下载该js文件,下载完成之后再执行。等 JS 引擎运行完毕,浏览器又会把控制权还给渲染线程,继续 DOM 的解析。
js阻塞问题是指当浏览器在解析文档或者渲染页面时,遇见了js代码,需要渲染引擎中断,而运行js引擎,从而阻塞浏览器原本的工作状态。
因此,js会阻塞DOM树的构建。
那么,是否会阻塞页面的显示呢?我们用下面的代码来测试一下。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>hello world</div>
<script>
debugger
</script>
<div>hello world2</div>
</body>
</html>
可以看到,这个页面的DOMContentLoaded发生在2.23s,可见js阻塞了DOM树的构建。但是,页面上却几乎在一瞬间显示了hello world,说明js不会阻塞位于它之前的dom元素的渲染。
现代浏览器为了更好的用户体验,渲染引擎将尝试尽快在屏幕上显示的内容。它不会等到所有DOM解析完成后才布局渲染树。而是当js阻塞发生时,会将已经构建好的DOM元素渲染到屏幕上,减少白屏的时间。
这也是为什么我们会将script标签放到body标签的底部,因为这样就不会影响前面的页面的渲染。
css 阻塞了什么
当我们解析 HTML 时遇到 link 标签或者 style 标签时,就会计算样式,构建CSSOM。
css不会阻塞dom树的构建,但是会阻塞页面的显示。我们依然用一个例子来测试:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" type="text/css" href="https://h5.sinaimg.cn/m/weibo-pro/css/chunk-vendors.d6cac585.css">
</head>
<body>
<div class="woo-spinner-filled">hello world</div>
<div>hello world2</div>
</body>
</html>
使用一个外部css文件,打开Slow 3G模拟比较慢的网速,可以看到,DOMContentLoaded事件触发只用了30ms,页面此时依然是空白,而几乎是loaded事件2.92s发生时,页面才出现内容。
原因是,浏览器在构建 CSSOM 的过程中,不会渲染任何已处理的内容。即便 DOM 已经解析完毕了,只要 CSSOM 没构建好,页面也不会显示内容。
只有当我们遇到 link 标签或者 style 标签时,才会构建CSSOM,所以如果 link 标签之前有dom元素,当加载css发生阻塞时,浏览器会将前面已经构建好的DOM元素渲染到屏幕上,以减少白屏的时间。比如下面这样:
// 首先显示黑色hello world,等待CSSOM加载完毕之后,显示hello world2,并且hello world变为红色
<body>
<div class="woo-spinner-filled">hello world</div>
<link rel="stylesheet" type="text/css" href="https://h5.sinaimg.cn/m/weibo-pro/css/chunk-vendors.d6cac585.css">
<div>hello world2</div>
</body>
这样做会导致一个问题,就是页面闪烁,在css被加载之前,浏览器按照默认样式渲染
hello world
,当css加载完成,会为该div计算新的样式,重新渲染,出现闪烁的效果。
为了避免页面闪烁,通常 link 标签都放在head中。
css会不会阻塞后面js执行?答案是会!
JS 的作用在于修改,它帮助我们修改网页的方方面面:内容、样式以及它如何响应用户交互。这“方方面面”的修改,本质上都是对 DOM 和 CSSDOM 进行修改。当在JS中访问了CSSDOM中某个元素的样式,那么这时候就需要等待这个样式被下载完成才能继续往下执行JS脚本。
运行下面这个例子,就会发现等css加载完成后,才会在控制台打印“this is a test”。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" type="text/css" href="https://h5.sinaimg.cn/m/weibo-pro/css/chunk-vendors.d6cac585.css">
</head>
<body>
<div class="woo-spinner-filled">hello world</div>
<div>hello world2</div>
<script>
console.log('this is a test')
</script>
</body>
</html>
优化
- 使用内联 JavaScript 和 CSS,这样获取到 HTML 文件之后就可以直接开始渲染流程了。
- 并不是所有的场合都适合内联,那么还可以尽量减少文件大小,比如通过 webpack 等构建工具删除无用代码、压缩 css、JavaScript 文件的体积;并且启用 CDN 加快文件的下载速度。
- 对于大的 CSS 文件,可以通过媒体查询属性,将其拆分为多个不同用途的 CSS 文件,这样只有在特定的场景下才会加载特定的 CSS 文件。
- 如果 JavaScript 文件中没有操作 DOM 相关代码,就可以将该 JavaScript 脚本设置为异步加载,通过
async或defer来标记代码。
代码如下:
<script src="index.js"></script>
//浏览器必须等待 index.js 加载和执行完毕才能去做其它事情。
<script async src="index.js"></script>
//index.js 的加载是异步的,加载时不会阻塞页面渲染,适用于js代码不需要操作DOM的。下载完毕之后立即加载,在另一个线程中加载,而不是Main Thread
<script defer src="index.js"></script>
//JS 的加载是异步的,执行是被推迟的。
//使用了 defer 标记的脚本文件,会等整个文档解析完成,在 DOMContentLoaded 事件触发之前执行,HTML5脚本规范规定它们按顺序执行。在实际当中,不一定按顺序执行或者在DOMContentLoaded事件之前执行
async、defer区别
1、共同点:
可以让浏览器异步加载js代码,为解决传统脚本加载阻塞 HTML 解析的问题而生。
2、区别:
- 加载方式:
defer:脚本会与 HTML 并行加载,但会等到 HTML 解析完成后(DOMContentLoaded 事件触发前)才执行。async:脚本会与 HTML 并行加载,加载完成后立即执行,可能会打断 HTML 的解析。
- 执行顺序:
defer:多个带defer的脚本会按照它们在 HTML 中出现的顺序执行。async:多个带async的脚本执行顺序不确定,先加载完成的先执行。
- 适用场景:
defer:适用于需要在 DOM 加载完成后执行,但顺序很重要的脚本,如操作 DOM 的库或框架。async:适用于独立的、不依赖其他脚本和 DOM 的脚本,如广告、分析脚本。
总结
- DOM树和CSSOM树是分别构建的,DOM树只有在CSSOM树构建好之后才会渲染,如果需要等待CSSOM树的构建,在页面上渲染link标签之前的DOM树,否则白屏。
- JavaScript的执行会阻塞DOM树的渲染,如果发生阻塞,渲染script标签之前的DOM树,否则白屏。
重排(Reflow)
概念:
当DOM的变化影响了元素的几何信息(DOM对象的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。
重排也叫回流,重排的过程以下面这种理解方式更清晰一些:
回流就好比向河里(文档流)扔了一块石头(dom变化),激起涟漪,然后引起周边水流受到波及,所以叫做回流
常见引起重排属性和方法
任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发重排,下面列一些栗子:
- 添加或者删除可见的DOM元素;
- 元素尺寸改变——边距、填充、边框、宽度和高度
- 内容变化,比如用户在input框中输入文字
- 浏览器窗口尺寸改变——resize事件发生时
- 计算 offsetWidth 和 offsetHeight 属性
- 设置 style 属性的值
| 常见引起重排属性和方法 | |||
|---|---|---|---|
| width | height | margin | padding |
| display | border | position | overflow |
| clientWidth | clientHeight | clientTop | clientLeft |
| offsetWidth | offsetHeight | offsetTop | offsetLeft |
| scrollWidth | scrollHeight | scrollTop | scrollLeft |
| scrollIntoView() | scrollTo() | getComputedStyle() | |
| getBoundingClientRect() | scrollIntoViewIfNeeded() |
重排影响的范围:
由于浏览器渲染界面是基于流失布局模型的,所以触发重排时会对周围DOM重新排列,影响的范围有两种:
- 全局范围:从根节点
html开始对整个渲染树进行重新布局。 - 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局
全局范围重排:
<body>
<div class="hello">
<h4>hello</h4>
<p><strong>Name:</strong>BDing</p>
<h5>male</h5>
<ol>
<li>coding</li>
<li>loving</li>
</ol>
</div>
</body>
当p节点上发生reflow时,hello和body也会重新渲染,甚至h5和ol都会收到影响。
局部范围重排:
用局部布局来解释这种现象:把一个dom的宽高之类的几何信息定死,然后在dom内部触发重排,就只会重新渲染该dom内部的元素,而不会影响到外界。
重绘(Repaint)
概念:
当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。
常见的引起重绘的属性
| color | border-style | visibility | background |
| text-decoration | background-image | background-position | background-repeat |
| outline-color | outline | outline-style | border-radius |
| outline-width | box-shadow | background-size |
浏览器的渲染队列:
思考以下代码将会触发几次渲染?
div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';
根据我们上文的定义,这段代码理论上会触发4次重排+重绘,因为每一次都改变了元素的几何属性,实际上最后只触发了一次重排,这都得益于浏览器的渲染队列机制:
当我们修改了元素的几何属性,导致浏览器触发重排或重绘时。它会把该操作放进渲染队列,等到队列中的操作到了一定的数量或者到了一定的时间间隔时,浏览器就会批量执行这些操作。
强制刷新队列:
div.style.left = '10px';
console.log(div.offsetLeft);
div.style.top = '10px';
console.log(div.offsetTop);
div.style.width = '20px';
console.log(div.offsetWidth);
div.style.height = '20px';
console.log(div.offsetHeight);
这段代码会触发4次重排+重绘,因为在console中你请求的这几个样式信息,无论何时浏览器都会立即执行渲染队列的任务,即使该值与你操作中修改的值没关联。
因为队列中,可能会有影响到这些值的操作,为了给我们最精确的值,浏览器会立即重排+重绘。
强制刷新队列的style样式请求:
- offsetTop, offsetLeft, offsetWidth, offsetHeight
- scrollTop, scrollLeft, scrollWidth, scrollHeight
- clientTop, clientLeft, clientWidth, clientHeight
- getComputedStyle(), 或者 IE的 currentStyle
我们在开发中,应该谨慎的使用这些style请求,注意上下文关系,避免一行代码一个重排,这对性能是个巨大的消耗。
如何减少重排、重绘?
1、最小化重绘和重排
由于重绘和重排可能代价比较昂贵,因此最好就是可以减少它的发生次数。为了减少发生次数,我们可以合并多次对DOM和样式的修改,然后一次处理掉。考虑这个例子
const el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
例子中,有三个样式属性被修改了,每一个都会影响元素的几何结构,引起回流。当然,大部分现代浏览器都对其做了优化,因此,只会触发一次重排。但是如果在旧版的浏览器或者在上面代码执行的时候,有其他代码访问了布局信息(上文中的会触发回流的布局信息),那么就会导致三次重排。
因此,我们可以合并所有的改变然后依次处理,比如我们可以采取以下的方式:
-
使用cssText
const el = document.getElementById('test'); el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;'; -
修改CSS的class
const el = document.getElementById('test'); el.className += ' active';
2、批量修改DOM
当我们需要对DOM对一系列修改的时候,可以通过以下步骤减少回流重绘次数:
- 使元素脱离文档流
- 对其进行多次修改
- 将元素带回到文档中。
该过程的第一步和第三步可能会引起回流,但是经过第一步之后,对DOM的所有修改都不会引起回流重绘,因为它已经不在渲染树了。
有三种方式可以让DOM脱离文档流:
- 隐藏元素,应用修改,重新显示
- 使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档。
- 将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。
考虑我们要执行一段批量插入节点的代码:
function appendDataToElement(appendToElement, data) {
let li;
for (let i = 0; i < data.length; i++) {
li = document.createElement('li');
li.textContent = 'text';
appendToElement.appendChild(li);
}
}
const ul = document.getElementById('list');
appendDataToElement(ul, data);
如果我们直接这样执行的话,由于每次循环都会插入一个新的节点,会导致浏览器回流一次。
我们可以使用这三种方式进行优化:
隐藏元素,应用修改,重新显示
这个会在展示和隐藏节点的时候,产生两次回流
function appendDataToElement(appendToElement, data) {
let li;
for (let i = 0; i < data.length; i++) {
li = document.createElement('li');
li.textContent = 'text';
appendToElement.appendChild(li);
}
}
const ul = document.getElementById('list');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';
使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档
const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
ul.appendChild(fragment);
将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。
const ul = document.getElementById('list');
const clone = ul.cloneNode(true);
appendDataToElement(clone, data);
ul.parentNode.replaceChild(clone, ul);
对于上面这三种情况,我写了一个demo在safari和chrome上测试修改前和修改后的性能。然而实验结果不是很理想。
原因:原因其实上面也说过了,现代浏览器会使用队列来储存多次修改,进行优化,所以对这个优化方案,我们其实不用优先考虑。
3、避免触发同步布局事件
上文我们说过,当我们访问元素的一些属性的时候,会导致浏览器强制清空队列,进行强制同步布局。举个例子,比如说我们想将一个p标签数组的宽度赋值为一个元素的宽度,我们可能写出这样的代码:
function initP() {
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px';
}
}
这段代码看上去是没有什么问题,可是其实会造成很大的性能问题。在每次循环的时候,都读取了box的一个offsetWidth属性值,然后利用它来更新p标签的width属性。这就导致了每一次循环的时候,浏览器都必须先使上一次循环中的样式更新操作生效,才能响应本次循环的样式读取操作。每一次循环都会强制浏览器刷新队列。我们可以优化为:
const width = box.offsetWidth;
function initP() {
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = width + 'px';
}
}
同样,我也写了个demo来比较两者的性能差异。你可以自己点开这个demo体验下。这个对比的性能差距就比较明显。
4、对于复杂动画效果,使用绝对定位让其脱离文档流
对于复杂动画效果,由于会经常的引起回流重绘,因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的回流。这个我们就直接上个例子。
打开这个例子后,我们可以打开控制台,控制台上会输出当前的帧数(虽然不准)。
从上图中,我们可以看到,帧数一直都没到60。这个时候,只要我们点击一下那个按钮,把这个元素设置为绝对定位,帧数就可以稳定60。
5、css3硬件加速(GPU加速)
比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘。这个时候,css3硬件加速就闪亮登场啦!!
划重点:
1. 使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。
2. 对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。
本篇文章只讨论如何使用,暂不考虑其原理,之后有空会另外开篇文章说明。
如何使用
常见的触发硬件加速的css属性:
- transform
- opacity
- filters
- Will-change
效果
我们可以先看个例子。我通过使用chrome的Performance捕获了动画一段时间里的回流重绘情况,实际结果如下图:
从图中我们可以看出,在动画进行的时候,没有发生任何的回流重绘。如果感兴趣你也可以自己做下实验。
重点
- 使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘
- 对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。
css3硬件加速的坑
当然,任何美好的东西都是会有对应的代价的,过犹不及。css3硬件加速还是有坑的:
- 如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。
- 在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。
面试题
1、构建渲染树时,渲染引擎遍历DOM树节点并从CSSOM树中找到匹配的样式规则,匹配过程中是通过自上而下还是自下而上的方式呢?
在浏览器匹配CSS规则构建渲染树时,匹配过程的核心是 自下而上(或说从右到左) 的规则匹配方式,而不是自上而下。
下面我详细解释一下:
核心匹配方向:从右到左
当浏览器为一个DOM节点计算样式时,它会检查所有匹配该元素的CSS规则。关键优化在于选择器匹配是从最右边的关键选择器开始,向左回溯。
示例说明
/* 规则1 */
div .container ul li a { color: blue; }
/* 规则2 */
a.active { color: red; }
对于元素 <a class="active">,匹配过程是:
- 先找到所有
a元素(最右边选择器) - 然后在这些
a元素中筛选有.active类的 - 对于规则1,先找到所有
a,然后检查父链是否符合li > ul > .container > div
为什么从右到左?
-
性能优化:
- 最右边的选择器(如
a、.active)通常能快速过滤掉大量不匹配的元素 - 如果最右边选择器不匹配,整个规则立即抛弃,无需检查左边的祖先条件
- 最右边的选择器(如
-
减少无效匹配:
- 页面中可能有很多
a标签,但只有少数有.active类 - 先过滤出
a.active再验证祖先结构,效率更高
- 页面中可能有很多
关键步骤:
- DOM遍历:通常是自上而下遍历DOM树节点
- 规则收集:为当前节点收集所有可能匹配的CSS规则
- 选择器验证:对每个规则从右到左验证选择器链
- 样式计算:按优先级(特异性、顺序)计算最终样式
性能启示
这就是为什么CSS选择器性能通常:
- ✅ 优先使用类选择器(
.class) - ✅ ID选择器很快(
#id) - ⚠️ 避免过度嵌套(
div ul li a span) - ❌ 慎用通用选择器(
*) - ❌ 避免属性选择器用于大量元素
总结
| 方面 | 方向 | 说明 |
|---|---|---|
| 选择器匹配 | 从右到左 | 性能优化,先过滤元素再验证祖先 |
| DOM遍历 | 自上而下 | 顺序处理DOM树节点 |
| 样式继承 | 自上而下 | 继承的样式从父到子传递 |
| 规则层叠 | 自下而上评估 | 比较特异性、顺序等 |
2、CSS阻塞了什么?
| 类型 | 是否阻塞DOM解析 | 是否阻塞DOM渲染 | 是否阻塞JS执行 |
|---|---|---|---|
| 普通CSS | ❌ 否 | ✅ 是 | 可能(访问样式时) |
| CSS带媒体查询 | ❌ 否 | 条件性阻塞 | 条件性阻塞 |
| 内联CSS | ❌ 否 | ❌ 否 | ❌ 否 |
CSS会阻塞DOM解析吗
CSS加载、解析默认不会阻塞DOM解析,但可能通过阻塞JS执行间接阻塞DOM解析。
- 浏览器在解析HTML构建DOM树时,如果遇到CSS链接,会继续解析后面的HTML内容
- DOM树的构建不会因为CSS加载而暂停
- 这是因为CSS不会修改DOM结构,所以可以并行处理
CSS会阻塞DOM渲染吗
CSS加载、解析会阻塞DOM渲染。
- 虽然DOM解析可以继续,但页面的渲染会被阻塞
- 浏览器需要等到CSSOM构建完成后,才会进行页面渲染(合成渲染树)
- 这是为了避免"无样式内容闪烁"(FOUC)的问题
CSS会阻塞CSS加载、解析吗
- CSS加载是并行的:浏览器会同时发起多个CSS文件的请求
- CSS解析是串行的:必须按照文档顺序依次解析
- 阻塞是必要的:为了保证CSS层叠规则的正确性
- 优化方法:通过合并文件、使用媒体查询、异步加载等技术减少阻塞时间
| 场景 | 后续CSS加载 | 后续CSS解析 | 页面渲染 |
|---|---|---|---|
| 普通CSS链接 | ✅ 不阻塞 | ❌ 阻塞 | ❌ 阻塞 |
| 带不匹配媒体查询的CSS | ✅ 不阻塞 | ✅ 不阻塞 | ✅ 不阻塞 |
| 动态插入的CSS | ✅ 不阻塞 | ✅ 不阻塞 | ✅ 不阻塞 |
使用preload的CSS | ✅ 不阻塞 | ❌ 阻塞(按顺序) | ❌ 阻塞 |
CSS会阻塞JS加载、执行吗
- CSS阻塞后续JS执行:浏览器必须保证JS执行时CSSOM已就绪
- CSS不阻塞JS加载:现代浏览器可以并行下载资源
- JS阻塞CSS加载:当CSS在JS之后时,JS会阻塞后续所有资源加载
- async脚本是特例:不会被CSS阻塞执行
- defer脚本会等待:等DOM和CSSOM都完成后才执行
| 场景 | JS加载 | JS执行 | 原因 |
|---|---|---|---|
| CSS → 普通JS | ❌ 不阻塞 | ✅ 阻塞 | JS可能访问样式 |
| CSS → async JS | ❌ 不阻塞 | ❌ 不阻塞 | async不等待 |
| CSS → defer JS | ❌ 不阻塞 | ⏳ 延迟执行 | 等DOM+CSSOM完成 |
| 普通JS → CSS | ✅ 阻塞 | N/A | JS可能修改DOM |
| async JS → CSS | ❌ 不阻塞 | N/A | async不阻塞解析 |
最佳实践:
- 将CSS放在头部,尽早开始加载
- 将JS放在底部或使用async/defer
- 内联关键CSS,异步加载非关键CSS
- 使用媒体查询优化CSS加载
CSS加载、解析不会阻塞JS加载。
CSS加载、解析可能会阻塞JS的执行。
- 如果JavaScript尝试访问样式属性,浏览器必须等待CSS加载完成
- 这种情况下,CSS会间接阻塞DOM解析(因为JS执行被阻塞)
示例:
<!DOCTYPE html>
<html>
<head>
<!-- CSS加载不会暂停DOM解析,但会阻塞渲染 -->
<link rel="stylesheet" href="slow.css?delay=2000">
</head>
<body>
<!-- 这些DOM元素会继续被解析 -->
<div id="content">内容会正常解析...</div>
<!-- 但页面不会渲染,直到CSS加载完成 -->
<script>
// 如果JS要访问样式,必须等待CSS加载
console.log('DOM已解析完成');
// 下面的代码会等待CSS加载
const div = document.getElementById('content');
console.log('offsetHeight:', div.offsetHeight); // 需要等待CSS
</script>
</body>
</html>
优化CSS阻塞
<!-- 1. 将CSS放在<head>中尽早加载 -->
<head>
<link rel="stylesheet" href="styles.css">
</head>
<!-- 2. 使用媒体查询避免不必要的阻塞 -->
<link rel="stylesheet" href="print.css" media="print"> <!-- 不阻塞屏幕渲染 -->
<link rel="stylesheet" href="mobile.css" media="(max-width: 600px)">
<!-- 3. 使用preload预加载关键CSS -->
<link rel="preload" href="critical.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="critical.css"></noscript>
<!-- 4. 内联关键CSS,避免阻塞 -->
<style>
/* 关键样式内联 */
.critical { color: red; }
</style>
3、JS阻塞了什么?
| 属性 | 阻塞解析 | 阻塞渲染 | 执行顺序 | 适用场景 |
|---|---|---|---|---|
| 同步脚本 | 阻塞解析 | 阻塞渲染 | 加载完立即执行,按文档顺序 | |
async | 不阻塞解析 | 执行时阻塞 | 加载完立即执行,顺序不定 | 独立脚本(如统计代码) |
defer | 不阻塞解析 | 不阻塞渲染 | DOM 解析完后按序执行 | 依赖 DOM 或需按序执行的脚本 |
| 动态加载 | 不阻塞解析 | 执行时阻塞 | 默认 async 行为 | 按需加载的模块 |
JS会阻塞DOM解析吗
JS加载、执行默认会阻塞dom解析,不过可以通过async、defer优化。
-
同步脚本(无
async/defer) :- 当 HTML 解析器遇到
<script>标签时,会暂停 DOM 解析,下载并执行脚本(如果是外链脚本,需先下载)。 - 原因:脚本中可能包含修改 DOM 的代码(如
document.write()或 DOM 操作),浏览器需保证脚本执行前的 DOM 状态是确定的。
- 当 HTML 解析器遇到
-
内联脚本:同样会阻塞解析,直到脚本执行完成。
JS会阻塞DOM渲染吗
默认会。
- JavaScript 执行期间,GUI渲染线程会被挂起(因为 JS 可能修改 DOM 或样式),导致页面渲染被阻塞。
- 即使脚本未修改 DOM,浏览器也会保守地暂停渲染,以避免脚本执行过程中的样式计算错误。
JS会阻塞JS加载、执行吗
JS加载、执行默认会阻塞后续JS加载、执行,但可以通过async、defer优化。
| 脚本类型 | 阻塞后续 JS 加载 | 阻塞后续 JS 执行 | 执行顺序 |
|---|---|---|---|
| 同步脚本 | ✅ 是 | ✅ 是 | 文档顺序 |
async | ❌ 否 | ✅ 执行时阻塞 | 加载顺序 |
defer | ❌ 否 | ✅ 按序阻塞 | 文档顺序 |
| 模块脚本(type=module) | ❌ 否 | ✅ 按序阻塞 | 文档顺序 |
JS会阻塞CSS加载、解析吗
- 加载阻塞:同步 JS 会阻塞后续 CSS 的发现和加载(除非预加载扫描器提前发现)
- 解析阻塞:JS 执行总是会阻塞 CSS 的解析和应用(因为渲染线程挂起)
- 依赖关系:JS 需要等待它之前的所有 CSS 解析完成
- 优化核心:让关键 CSS 尽早加载,让非关键 JS 异步执行
| 场景 | CSS 加载被阻塞 | CSS 解析被阻塞 | 根本原因 |
|---|---|---|---|
| 同步 JS 在前 | ✅ 是 | ✅ 是 | DOM 解析暂停,无法发现/处理后续资源 |
| 同步 JS 在后 | ❌ 否 | ⚠️ 部分阻塞 | JS 需等待前面 CSS 解析完成 |
| async JS 在前 | ❌ 否 | ⚠️ 执行时阻塞 | 不阻塞发现,但执行时阻塞渲染线程 |
| defer JS 在前 | ❌ 否 | ❌ 否 | 完全不阻塞,最后执行 |
| CSS 在同步 JS 前 | ❌ 否 | ✅ 是 | JS 执行需要完整的 CSSOM |
参考:
浏览器重绘(repaint)重排(reflow)与优化[浏览器机制]
深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调(动图演示)