浏览器资源加载、页面渲染机制

45 阅读25分钟

渲染流程:

1.解析HTML,生成DOM树(DOM)

2.解析CSS,生成CSSOM树(CSSOM)

3.将DOM和CSSOM合并,生成渲染树(Render-Tree)

4.计算渲染树的布局(Layout)

5.将布局渲染到屏幕上(Paint)

生成渲染树

生成渲染树

为了构建渲染树,浏览器主要完成了以下工作:

  1. 从DOM树的根节点开始遍历每个可见节点。
  2. 对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。
  3. 根据每个可见节点以及其对应的样式,组合生成渲染树。

第一步中,既然说到了要遍历可见的节点,那么我们得先知道,什么节点是不可见的。不可见的节点包括:

  • 一些不会渲染输出的节点,比如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 脚本设置为异步加载,通过 asyncdefer 来标记代码。

代码如下:

<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、区别:

  1. 加载方式
  • defer:脚本会与 HTML 并行加载,但会等到 HTML 解析完成后(DOMContentLoaded 事件触发前)才执行
  • async:脚本会与 HTML 并行加载,加载完成后立即执行,可能会打断 HTML 的解析
  1. 执行顺序
  • defer:多个带defer的脚本会按照它们在 HTML 中出现的顺序执行
  • async:多个带async的脚本执行顺序不确定,先加载完成的先执行
  1. 适用场景
  • defer:适用于需要在 DOM 加载完成后执行,但顺序很重要的脚本,如操作 DOM 的库或框架。
  • async:适用于独立的、不依赖其他脚本和 DOM 的脚本,如广告、分析脚本。

总结

  • DOM树和CSSOM树是分别构建的,DOM树只有在CSSOM树构建好之后才会渲染,如果需要等待CSSOM树的构建,在页面上渲染link标签之前的DOM树,否则白屏。
  • JavaScript的执行会阻塞DOM树的渲染,如果发生阻塞,渲染script标签之前的DOM树,否则白屏。

重排(Reflow)

概念:

当DOM的变化影响了元素的几何信息(DOM对象的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。

重排也叫回流,重排的过程以下面这种理解方式更清晰一些:

回流就好比向河里(文档流)扔了一块石头(dom变化),激起涟漪,然后引起周边水流受到波及,所以叫做回流

常见引起重排属性和方法

任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发重排,下面列一些栗子:

  1. 添加或者删除可见的DOM元素;
  2. 元素尺寸改变——边距、填充、边框、宽度和高度
  3. 内容变化,比如用户在input框中输入文字
  4. 浏览器窗口尺寸改变——resize事件发生时
  5. 计算 offsetWidth 和 offsetHeight 属性
  6. 设置 style 属性的值
常见引起重排属性和方法
widthheightmarginpadding
displayborderpositionoverflow
clientWidthclientHeightclientTopclientLeft
offsetWidthoffsetHeightoffsetTopoffsetLeft
scrollWidthscrollHeightscrollTopscrollLeft
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)

概念

当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。

常见的引起重绘的属性

colorborder-stylevisibilitybackground
text-decorationbackground-imagebackground-positionbackground-repeat
outline-coloroutlineoutline-styleborder-radius
outline-widthbox-shadowbackground-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样式请求

  1. offsetTop, offsetLeft, offsetWidth, offsetHeight
  2. scrollTop, scrollLeft, scrollWidth, scrollHeight
  3. clientTop, clientLeft, clientWidth, clientHeight
  4. 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对一系列修改的时候,可以通过以下步骤减少回流重绘次数:

  1. 使元素脱离文档流
  2. 对其进行多次修改
  3. 将元素带回到文档中。

该过程的第一步和第三步可能会引起回流,但是经过第一步之后,对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、对于复杂动画效果,使用绝对定位让其脱离文档流

对于复杂动画效果,由于会经常的引起回流重绘,因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的回流。这个我们就直接上个例子

打开这个例子后,我们可以打开控制台,控制台上会输出当前的帧数(虽然不准)。

image-20181210223750055

从上图中,我们可以看到,帧数一直都没到60。这个时候,只要我们点击一下那个按钮,把这个元素设置为绝对定位,帧数就可以稳定60。

5、css3硬件加速(GPU加速)

比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘。这个时候,css3硬件加速就闪亮登场啦!!

划重点:

1. 使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。

2. 对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

本篇文章只讨论如何使用,暂不考虑其原理,之后有空会另外开篇文章说明。

如何使用

常见的触发硬件加速的css属性:

  • transform
  • opacity
  • filters
  • Will-change
效果

我们可以先看个例子。我通过使用chrome的Performance捕获了动画一段时间里的回流重绘情况,实际结果如下图:

image-20181210225609533

从图中我们可以看出,在动画进行的时候,没有发生任何的回流重绘。如果感兴趣你也可以自己做下实验。

重点
  • 使用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">,匹配过程是:

  1. 先找到所有 a 元素(最右边选择器)
  2. 然后在这些 a 元素中筛选有 .active 类的
  3. 对于规则1,先找到所有 a,然后检查父链是否符合 li > ul > .container > div

为什么从右到左?

  1. 性能优化

    • 最右边的选择器(如 a.active)通常能快速过滤掉大量不匹配的元素
    • 如果最右边选择器不匹配,整个规则立即抛弃,无需检查左边的祖先条件
  2. 减少无效匹配

    • 页面中可能有很多 a 标签,但只有少数有 .active 类
    • 先过滤出 a.active 再验证祖先结构,效率更高

关键步骤:

  1. DOM遍历:通常是自上而下遍历DOM树节点
  2. 规则收集:为当前节点收集所有可能匹配的CSS规则
  3. 选择器验证:对每个规则从右到左验证选择器链
  4. 样式计算:按优先级(特异性、顺序)计算最终样式

性能启示

这就是为什么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加载、解析吗

  1. CSS加载是并行的:浏览器会同时发起多个CSS文件的请求
  2. CSS解析是串行的:必须按照文档顺序依次解析
  3. 阻塞是必要的:为了保证CSS层叠规则的正确性
  4. 优化方法:通过合并文件、使用媒体查询、异步加载等技术减少阻塞时间
场景后续CSS加载后续CSS解析页面渲染
普通CSS链接✅ 不阻塞❌ 阻塞❌ 阻塞
带不匹配媒体查询的CSS✅ 不阻塞✅ 不阻塞✅ 不阻塞
动态插入的CSS✅ 不阻塞✅ 不阻塞✅ 不阻塞
使用preload的CSS✅ 不阻塞❌ 阻塞(按顺序)❌ 阻塞

CSS会阻塞JS加载、执行吗

  1. CSS阻塞后续JS执行:浏览器必须保证JS执行时CSSOM已就绪
  2. CSS不阻塞JS加载:现代浏览器可以并行下载资源
  3. JS阻塞CSS加载:当CSS在JS之后时,JS会阻塞后续所有资源加载
  4. async脚本是特例:不会被CSS阻塞执行
  5. defer脚本会等待:等DOM和CSSOM都完成后才执行
场景JS加载JS执行原因
CSS → 普通JS❌ 不阻塞✅ 阻塞JS可能访问样式
CSS → async JS❌ 不阻塞❌ 不阻塞async不等待
CSS → defer JS❌ 不阻塞⏳ 延迟执行等DOM+CSSOM完成
普通JS → CSS✅ 阻塞N/AJS可能修改DOM
async JS → CSS❌ 不阻塞N/Aasync不阻塞解析

最佳实践

  • 将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 状态是确定的。
  • 内联脚本:同样会阻塞解析,直到脚本执行完成。

JS会阻塞DOM渲染吗

默认会

  • JavaScript 执行期间,GUI渲染线程会被挂起(因为 JS 可能修改 DOM 或样式),导致页面渲染被阻塞。
  • 即使脚本未修改 DOM,浏览器也会保守地暂停渲染,以避免脚本执行过程中的样式计算错误。

JS会阻塞JS加载、执行吗

JS加载、执行默认会阻塞后续JS加载、执行,但可以通过async、defer优化。

脚本类型阻塞后续 JS 加载阻塞后续 JS 执行执行顺序
同步脚本✅ 是✅ 是文档顺序
async❌ 否✅ 执行时阻塞加载顺序
defer❌ 否✅ 按序阻塞文档顺序
模块脚本(type=module)❌ 否✅ 按序阻塞文档顺序

JS会阻塞CSS加载、解析吗

  1. 加载阻塞:同步 JS 会阻塞后续 CSS 的发现和加载(除非预加载扫描器提前发现)
  2. 解析阻塞:JS 执行总是会阻塞 CSS 的解析和应用(因为渲染线程挂起)
  3. 依赖关系:JS 需要等待它之前的所有 CSS 解析完成
  4. 优化核心:让关键 CSS 尽早加载,让非关键 JS 异步执行
场景CSS 加载被阻塞CSS 解析被阻塞根本原因
同步 JS 在前✅ 是✅ 是DOM 解析暂停,无法发现/处理后续资源
同步 JS 在后❌ 否⚠️ 部分阻塞JS 需等待前面 CSS 解析完成
async JS 在前❌ 否⚠️ 执行时阻塞不阻塞发现,但执行时阻塞渲染线程
defer JS 在前❌ 否❌ 否完全不阻塞,最后执行
CSS 在同步 JS 前❌ 否✅ 是JS 执行需要完整的 CSSOM

参考:

浏览器的渲染:过程与原理

浏览器页面资源加载过程与优化

浏览器加载及渲染机制

浏览器页面渲染机制,你真的弄懂了吗

你不知道的浏览器页面渲染机制

渲染树的形成原理你真的很懂吗?

你真的了解回流和重绘吗

浏览器重绘(repaint)重排(reflow)与优化[浏览器机制]

浏览器的回流与重绘 (Reflow & Repaint)

重排(reflow)和重绘(repaint)

原来 CSS 与 JS 是这样阻塞 DOM 解析和渲染的

关于 JS 与 CSS 是否阻塞 DOM 的渲染和解析

深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调(动图演示)

从Chrome源码看浏览器如何加载资源

从输入URL到页面加载的过程?如何由一道题完善自己的前端知识体系!

前端性能优化:细说JavaScript的加载与执行

再谈 load 与 DOMContentLoaded

浏览器工作机制及资源加载

浏览器页面资源加载过程与优化

反直觉!浏览器到底是如何下载资源的

【浏览器运行原理】全程高能动画解析浏览器运行机制!

JavaScript高级 | 浏览器渲染过程

DOMContentLoaded说明

DOMContentLoaded与load的区别

DOMContentLoaded和load的区别--guo&qi