Html中Css和script的阻塞情况分析

背景

今天又是碰到诡异bug的一天。我们运营会用到 Google Analytics 埋点。埋点sdk的安装脚本大体都是这样的:

html 复制代码
  <script>
  !(function(w,d,s,l,i) {
    w[l]=w[l]||[];
    var f=d.getElementsByTagName(s)[0],
        j=d.createElement(s);
        j.async=true;
        j.src=i;
        f.parentNode.insertBefore(j,f);
  })(window, document, 'script', 'dataLayer', 'https://www.google-analytics.com/analytics.js');    
  </script>

动态创建一个带 async 的 script 标签请求埋点脚本。正常来说,加了 async 的脚本下载是不会阻塞页面渲染的。

然而今天在 ios webview (wkwebview) 中,用国内网络访问这个国内不存在的资源时,长时间的loading阻塞了首屏渲染。而用移动端的 Safari 打开又不会有阻塞问题。并且也不是每个页面都出现这个情况。

一开始怀疑是业务代码的问题,于是一行行排查,但最后发现,删除页面css中的某一行时,页面就不阻塞了。并且每个有问题的页面,需要删除的css都不一致,没什么特殊性。

那完犊子了,一行css的增删怎么会阻塞页面渲染,而且只在ios webview中出现,十有八九是webview的bug了。

一番搜索,只找到个类似的问题 脚本下载会阻塞 Mobile Safari 首屏渲染,虽然不一样,但解决思路是可以借鉴的。就是通过> requestAnimationFrame 和 microTask 延迟了 script 脚本的下载,使其在首屏渲染之后再开始下载;

问题解决了,但引发了我对css和js加载导致页面阻塞这个经典问题的思考,之前只是死记一直没有实践过,本着没有困难创造困难也要上的精神,特此水文一篇!

前置知识

  • 当页面因为加载外部资源而阻塞时,页面可能会优先渲染已经解析好的内容

以 chrome 为例,以下是一张经典老图,我们知道从文档开始解析到页面展示,主要经历了 html的解析,dom树的构建,css的解析,cssom的构建,再通过二者创建render tree,重排,重绘,渲染;而我们常遇到的阻塞问题,主要发生在 html解析 和 页面渲染。

需要注意的是,这是一个渐进的过程。为了更好的用户体验,渲染引擎会尽可能快的渲染内容,而不是等待所有的 html解析完成。也就是说,当页面因为加载外部资源而阻塞时,页面可能会优先渲染已经解析好的内容。

  • 在解析文档时,浏览器会有额外的线程找出需要下载的外部资源(js/css/img),并开始并行下载,也就是说html解析阻塞并不影响资源下载;

环境准备

起一个 parsing-demo 文件夹,内容如下

  • index.html
html 复制代码
<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>前端一块砖</title>
</head>
<body>
  <div id="up">上</div> 
  <img width="50" height="50" src="http://www.test.com/test.jpg" alt="">
  <div id="down">下</div> 
</body>
</html>
  • dist/test.js
js 复制代码
console.log('-------- test 执行啦 ---------')
  • dist/sleep.js
js 复制代码
function sleep(delay) {
  var start = new Date().getTime();
  while (new Date().getTime() - start < delay) {
    continue;
  }
  console.log('sleep done');
}
sleep(5000);
  • dist/test.jpg
  • dist/test.css
css 复制代码
#up {
  background-color: pink;
}
#down {
  background-color: skyblue;
}

因为懒得再起个服务器,我这里主要用 Charles 打断点来模拟请求阻塞

我这里是macos,先用 helm 配置一下 www.test.com 的 DNS 解析,解析到本地

127.0.0.1 www.test.com

然后执行

shell 复制代码
npx http-server dist -p 80

通过 http-server 起一个静态服务器,指定dist为根目录,通过参数 -p 设置端口为 80,这样就可以模拟访问外部资源啦

再开个终端执行

shell 复制代码
npx http-server

不指定额外参数,这样 http://127.0.0.1:8080/ 默认访问的就是我们的 index.html 了,测试下

页面可以正常访问;

最后再配置一个 Charles 的断点,用来拦截所有对 www.test.com 请求的响应,模拟下载阻塞(不会Charles的赶紧学啦!)

外部Css的阻塞情况分析

我们在head里加上css

html 复制代码
<head>
  <meta charset="utf-8" />
  <title>前端一块砖</title>
  <link rel="stylesheet" href="http://www.test.com/test.css">
</head>

访问 http://127.0.0.1:8080/

可以看到css的响应被正常拦截,页面一片空白,而 elements 已经能看到 html 的全部内容了;

放开 test.css 的断点,可以看到页面正常渲染

这样就验证了,外部css的下载不会阻塞 html 的解析,但会阻塞页面的渲染

这个很好理解,因为 css 并不能修改 dom,所以不应该阻塞 html 的解析,而 css 是可以修改页面样式的,所以会阻塞页面渲染;

大部分没有钻研精神的人都到此为止了。但!这只是把 <link> 放在 <head> 的情况,那么如果把 <link> 放在 <body> 里,结论还一样吗?(其实是我一开始手欠放错位置了...坑了我好久😭)

我们把 <link> 放到 <body> 里再试试看

可以看到页面在 link 加载时,阻塞了剩余 html 的解析,并且优先渲染了已解析的 html;这让我一度怀疑网上的文章是不是都是乱写的。我又拿 safari 和 firefox 测试了以下,发现三家表现都不一样;

safari 不阻塞解析,阻塞渲染

firefox 既不阻塞html解析 ,也不阻塞渲染

这时验证的结果已经和我们平时看到的文章有些出入了

script 的阻塞情况分析

我们知道 js 是可以修改 dom 的,为了防止html的解析和预期不一致,因此的 js 的下载和执行都会阻塞 html 解析,我们可以用以下例子验证。断点解开前,html解析阻塞;先解开 test.js 的断点,再解开 sleep.js 的断点,在sleep执行期间,html解析也会阻塞;

html 复制代码
<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>前端一块砖</title>
  <script src="http://www.test.com/sleep.js"></script>
</head>
<body>
  <div id="up">上</div> 
  <img width="50" height="50" src="http://www.test.com/test.jpg" alt="">
  <script src="http://www.test.com/test.js"></script>
  <div id="down">下</div> 
</body>
</html>

那么会不会阻塞渲染呢?

为了让页面有东西可渲染,script 就要放到 body 里了,发现三家表现又不一致;

chrome 和 safari 阻塞了 html 解析,优先渲染了已解析的内容

而 firefox 既阻塞了解析又阻塞了已解析内容的渲染

验证到这里,事情已经开始有点不对劲了🤨

我们再验证一下 当文档 link的css后面有 script 时,script 需要等待前面的css下载执行完毕才会执行,也就是说css的下载会阻塞后方js的执行,导致html解析的阻塞,我们可以用下面这段例子验证

html 复制代码
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>前端一块砖</title>
    <link rel="stylesheet" href="http://www.test.com/test.css">
  </head>
<body>
  <div id="up">上</div> 
  <img width="50" height="50" src="http://www.test.com/test.jpg" alt="">
  <script src="http://www.test.com/test.js"></script>
  <div id="down">下</div> 
</body>
</html>

可以看到 test.js 下载时阻塞了 html 的解析,我们先断点放行 js 的下载,继续阻塞 css,在 chrome 中页面依然没渲染,下载好的js也没有执行;

这是因为 css 可能会改变页面样式,而导致后面 js 获取的页面样式可能和预期不一致(因为 css 可能比js先下载好,也可能比js晚下载好),为了避免不符合预期的情况,js 的执行需要等待 css 下载执行完成(内联的 script 也是一样);

以上都是对于静态 script 标签的情况,而对于动态创建的 script 标签:无论是否带 async 标记的,标签创建完成都会立即开始下载,下载过程中都不会阻塞html解析和渲染,并且下载完成后立即执行

以上就是 css 和 script 在 html 解析过程中的大部分情况了;

回到上面各浏览器表现不一致的问题,其实就是各浏览器的实现不一致,各浏览器都有自家的渲染时机。可以自行验证一下,link 和 script 无论是放在 <head> 还是 <body> 中,各家表现其实都不一致。所以对于 css 和 script 导致的 html 解析阻塞和渲染阻塞并没有一个标准的答案。我觉得也没有必要去硬记各家的表现,具体情况具体分析就好了,如果说非要一个面试的答案的话,以下是基于 Chrome 的总结

总结

基于 chrome 119.0.6012.0(正式版本)canary (arm64)

  1. 阻塞通常发生在 html 解析 和 页面渲染阶段
  2. 文档解析时,浏览器会有额外线程找出需要下载的外部资源,提前开始下载,html解析阻塞不会影响外部资源下载
  3. css,当 link css 放在 head 时,css 的下载不会阻塞 html 的解析,但会阻塞页面的渲染,当 link 放在 body 时,阻塞 html 解析,不阻塞已解析部分的渲染
  4. script,静态script标签的下载和执行阻塞html的解析,但会先渲染已解析部分;script 之前有 link css 时,会等待css下载执行完成再执行
  5. 动态创建的script标签不会阻塞html解析,创建完成立即开始下载,下载完成立即执行,执行时阻塞html解析

参考

脚本下载会阻塞 Mobile Safari 首屏渲染

How browsers work

相关推荐
hackeroink38 分钟前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者2 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-3 小时前
验证码机制
前端·后端
燃先生._.4 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖5 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235245 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240256 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar6 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人6 小时前
前端知识补充—CSS
前端·css