HTML中为什么CSS要写在上面JS要写在下面

本文不涉及什么原理性知识,都是实例的推论,要是一定有和原理相关的那可能就是(可能还不见得对,哈哈哈哈!):

  1. 浏览器的基本渲染流程:构建DOM树 => 构建CSSOM树 => 构建Render树 => 布局Render树 => 绘制Render树
  2. 浏览器的单线程导致解析渲染和js的执行不能并存
  3. 浏览器的解析渲染是同步进行的,不是一口气解析完了再一口气渲染再一口气绘制

为什么CSS要写到文件上方的header里?

因为CSS不阻塞线程吗?其实外链的CSS是阻塞的,只不过不阻塞DOM树的构建,遇到link标签,浏览器会异步加载网络上的CSS资源,并往下加载DOM节点,但是并没有额外的渲染发生,等到资源请求回来,浏览器会整合所有的样式表进行统一的渲染和绘制,防止重绘和回流的发生。

我们来写一个简单的html页面,代码如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title></title>
    <style type="text/css">
        div{
            width: 100px;
            height: 100px;
            border: 1px solid red;
        }
        .a{
            background: linear-gradient(red,yellow,red)
        }
    </style>
</head>
<body>
    <div class="a pro_bg another-box" id="test">123123</div>
    <link rel="stylesheet" type="text/css" href="这个样式表里面有pro_bg会给div加个背景图,another-box会改变div形状">
    <p>这段文字会等待css加载完才出现</p>
</body>
</html>

打开弱网运行html,会发生什么?如果CSS什么都不阻塞,是不是会显示一个红边框有渐变色的100px的正方形和下面一段文字,等CSS回来,再把div改变?还是什么都不发生,最后等待CSS回来一口气把页面全部渲染,这样还能减少重绘和回流。

但是实际是这样的:先渲染一个红边框渐变色的正方形,然后加载CSS,等CSS加载完毕后渲染一个压扁的长方形和P标签,最后加载背景图资源。这说明了外链的CSS文件会阻塞页面的绘制。

所以为什么CSS要都写在最前面,因为页面不会中途遇到<link>的CSS资源产生多次绘制,如果一路上没有遇到CSS和JS,页面是一边解析一边渲染,最后一口气绘制的。

为什么JS要写到文件的底部?

因为js的运行是要占用主线程的,也就是在js运行的时候,页面是不进行解析和渲染的,如果js写在最前面,如果不做特殊处理,除了能提供依赖以外和做一些预处理之外,连dom节点都无法捕捉,导致功能受到了很大的限制

我们看下面这一段代码

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title></title>
    <style type="text/css">
        div{
            width: 100px;
            height: 100px;
            border: 1px solid red;
        }
        .a{
            background: linear-gradient(red,yellow,red)
        }
        .b{
            background: linear-gradient(green,blue,yellow)
        }
        .c{
            background: linear-gradient(black,gray,white)
        }
    </style>
</head>
<body>
    <div class="a pro_bg white-box-title" id="test">123123</div>

    <link rel="stylesheet" type="text/css" href="改变a的css">
    <script src="一个10亿次的空循环"></script>

    <div class="b">b</div>
    <div class="c">c</div>
</body>
</html>

这段代码在弱网情况下会发生什么?答案是:

  1. 显示一个红边框渐变色北京的正方形div(如果不是弱网这步肉眼观察不到)
  2. 加载CSS,大概2~3s后css加载回来,改变了第一个div的形状,背景色消失,开始执行js的大循环(这里证明js被css阻塞了,但是下面两个div没有被渲染,说明css异步挂载后,本应该执行的dom解析被js阻塞了)
  3. 循环结束,绘制剩下两个div
  4. 加载并绘制第一个div的背景图片

PS: 浏览器的行为很迷(应该是和帧渲染有一定关系,这一帧我正好渲染了,也有可能没有渲染)弱网强网,本页刷新,新标签页打开,开调适的页面和不开调适的页面,渲染的顺序都会有较小概率出现不同,比如强网的时候渲染顺序十分不稳定,有时候会直接出现一个带背景图的压缩div,有时候也会出现渐变色的正方形,然后执行js,再出现后两个div,甚至有时候还能直接出现3个div再执行for循环。

这里补充一下,如果没有<link>标签在a和bc之间插入一个script,也就是把上方代码的<link>删除,结果是:先显示a,script执行完毕再显示bc

顶部CSS+底部JS的优势

再看一段代码,对比前面在body里的CSS文件,如果把CSS写在header里,又会发生什么:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title></title>
    <link rel="stylesheet" type="text/css" href="http://res.haibian.com/tower/modules/course/index/style/style.css?v=20180719001010">
    <style type="text/css">
        div{
            width: 100px;
            height: 100px;
            border: 1px solid red;
        }
        .a{
            background: linear-gradient(red,yellow,red)
        }
        .b{
            background: linear-gradient(green,blue,yellow)
        }
        .c{
            background: linear-gradient(black,gray,white)
        }
    </style>
</head>
<body>

    <div class="a pro_bg white-box-title" id="test">123123</div>
    <script src="./for-code.js"></script> 

    <div class="b">b</div>
    <div class="c" id="ccc">c</div>
</body>
</html>

按照前面的结论,无论遇到<script>还是<link>浏览器都会对已经完成解析渲染的节点进行绘制。但是如果CSS文件在最上方,一上来就会对渲染进行阻塞,导致到了script标签的时候并没有已经渲染好的节点,只有一个<div class="a...">对应的dom结构,所以不会输出任何视图,在js执行结束后开始之后DOM的解析和节点的渲染,3个div是一口气出来的。

总结一下 CSS在上JS在下的好处:CSS一上来就会异步挂载,挂载期间进行dom树的构建,CSS加载完毕DOM也解析完毕后会渲染并绘制,如果CSS未加载完但DOM解析完毕则完会等待CSS,如果CSS加载完成但DOM没有解析完,继续往下解析DOM。

遇到最下方的JS时,至少所有的DOM节点都已经解析完毕,根据上面CSS+JS组合的结论,你会发现JS会在CSS加载完毕并完成绘制现有DOM节点后才会开始执行,不会出现先蹦出来一块再蹦出来另一坨的情况,一切OK!

如何防止<script>标签阻塞线程

给<script>标签添加async,可以让js在最后执行

有兴趣你可以试试setTimeout(function(){......},0)

// 无延迟的js
console.log('begin')
console.time('for')
console.log(document.getElementById('ccc'))
for(var i = 0; i<1000000000; i++){
}
console.timeEnd('for')

// 延迟0ms的js
console.log('js-begin')
setTimeout(function(){
    console.log('settimeout-begin')
    console.time('settimeout')
    console.log(document.getElementById('ccc'))
    for(var i = 0; i<1000000000; i++){
    }
    console.timeEnd('settimeout')
},0)

怎么说呢,很微妙,绘制的顺序会受到影响,经过我的可劲尝试,得到以下结论:

  1. <link>标签在header的时候,和之前不同,会直接渲染出三个div,然后再开始执行循环,能够在js内拿到<script>标签后面id为ccc的节点,但是并不是直接拿到的,执行到console.log(document.getElementById('ccc'))的时候,没有输出null也没有输出节点,而是空白,在for执行完成后输出<div class="c" id="ccc">c</div>,如果是同步代码,这个输出直接就是null

  2. <link>标签在body的时候(放在<div class="a ...">的后面),页面会因为<link>标签的存在先绘制一个div,后续不太稳定,下面两种情况都很容易复现:
    情况1:先绘制剩下两个div,最后执行js;
    情况2:先执行js,最后绘制剩下两个div;
    但无论那种,js都能够输出<div class="c" id="ccc">c</div>,过程和上方一样,也是先输出空白,最后再js执行结束后再输出结果,所以这里推断是,js肯定是没有阻塞DOM树的构建,也应该没有阻塞渲染和绘制,只是正好那一帧结束浏览器没有进行绘制。

PS:dom节点的输出 先空后有 是因为引用类型造成的,举个最简单的例子:

let a = [{a:123}, 2, 3]
console.log(a) // [{a: 456}, 2, 3]
a[0].a = 456

比较有意思的是,写在setTimeout里的大循环执行速度要更快,速度大概在4倍左右,10亿次空循环,setTimeout内耗时587.291015625ms,正常写耗时2276.846923828125ms。这里有待考究

转载于:https://www.jianshu.com/p/454f63169bff

猜你喜欢

转载自blog.csdn.net/weixin_33963594/article/details/91157338