为什么script脚本要放在body标签的最后面?

这是面试的时候经常遇到的问题,但是很少听到有人能完整的回答出来。其实这个问题并不简单,它涉及到浏览器渲染方面的知识,搞懂了这一块对就能对性能优化有一个比较清晰的认识。下面我将从基础部分一点点的讲透这个问题。

什么是 DOM

当请求页面时,网络传给渲染引擎的是 HTML文件字节流,它是无法直接被渲染引擎理解的,所以要将其转化为渲染引擎能够理解的内部结构。

在渲染引擎内部,有一个叫 HTML 解析器(HTMLParser) 的模块,它的职责就是负责将 HTML 字节流转换为 DOM 结构。

DOM 从 JavaScript 脚本视角来看,DOM 提供给 JavaScript 脚本操作的接口,通过这套接口,JavaScript 可以对 DOM 结构进行访问,从而改变文档的结构、样式和内容

首先看一个问题:HTML 解析器是等整个 HTML 文档加载完成之后开始解析的,还是随着 HTML 文档边加载边解析的?

答案是:HTML 解析器并不是等整个文档加载完成之后再解析的,而是网络进程加载了多少数据,HTML 解析器便解析多少数据。

知道了什么 DOM,那接下来看看 JavaScript 是如何影响 DOM 生成的。

JavaScript 是如何页面渲染的

<html>
<body><div>1</div><script> let div1 = document.getElementsByTagName('div')[0]div1.innerText = '123' </script><div>test</div>
</body>
</html>

HTML 解析器开始解析html字符串,当遇到 script 脚本后,HTML 解析器暂停工作,浏览器会渲染已经解析好了 DOM 结构,即把script 脚本之前的内容进行渲染。

大概意思是,浏览器在解析时,如果遇到了script标签,会先渲染一次这个script标签之前的DOM,然后再去加载和执行js。因为js是可以操作DOM的,如果浏览器不先去渲染一次,js获取的DOM就会是null。比如:

<body><p id="box1">world</p><script> console.log(document.getElementById('box1'));console.log(document.getElementById('box2')); </script><p id="box2">hello</p>
</body>

上面代码中 js 是可以获取到 id 为 box1 的p标签,但是不能获取 box2 的p标签,原因很简单,因为 script 在box2前面。js执行的时候,box2还没有被解析和渲染。

当渲染完成后,JavaScript 引擎介入,并执行 script 标签中的这段脚本,因为这段 JavaScript 脚本修改了 DOM 中第一个 div 中的内容。脚本执行完成之后,HTML 解析器恢复解析过程,继续解析后续的内容,直至生成最终的 DOM。

除了在页面中直接内嵌 JavaScript 脚本之外,我们还通常需要在页面中引入 JavaScript 文件,其实整个执行流程还是一样的,执行到 JavaScript 标签时,暂停整个 DOM 的解析,浏览器渲染 script 脚本之前的DOM结构,最后执行 JavaScript 代码。

不过这里执行 JavaScript 时,需要先下载这段 JavaScript 代码。这里需要重点关注下载环境,因为 JavaScript 文件的下载过程会阻塞 DOM 解析,而通常下载又是非常耗时的,会受到网络环境、JavaScript 文件大小等因素的影响

其实到这里,我们就能部分回答文章的提出的问题了,如果你把 script 放在最前面,同时大部分情况下 js 脚本都是放在服务器中的,因此会阻碍 html 的解析,这是会导致浏览器的白屏。

如果把 script 脚本放在最后面,当遇到 script 脚本时,浏览器其实已经解析完了脚本之前的大部分 html,因此浏览器会渲染出这部分页面,不至于屏幕上什么都没有,导致白屏。

你可以参考下面的代码进行测试:

服务端代码:

const express = require('express')
const fs = require('fs')
const path = require('path')const app = express()app.get('/render', (req, res) => {// 对js脚本设置延时setTimeout(() => {fs.readFile(path.join(__dirname, './render.js'), (err, result) => {res.setHeader('Content-Type', 'text/javascript')res.setHeader('Access-Control-Allow-Origin', 'http://localhost:52330/')res.end(result)})}, 3000)
})app.listen('8001', () => {console.log('running at 8001')
})

前端代码:

<!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 id="app">test</div><script src="http://localhost:8001/render"></script><div id="app1">执行完脚本之后的内容</div></body>
</html>

浏览器页面首先会出现 test 的文字,大约3s后才会出现后面的文字,这充分说明 script 脚本严重阻碍了 html的解析和渲染。

不过 Chrome 浏览器做了很多优化,其中一个主要的优化是预解析操作。当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析 HTML 文件中包含的 JavaScript、CSS 等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件

我们知道引入 JavaScript 线程会阻塞 DOM,不过也有一些相关的策略来规避:

  • 使用 CDN 来加速 JavaScript 文件的加载
  • 压缩 JavaScript 文件的体积
  • 如果 JavaScript 文件中没有操作 DOM 相关代码,就可以将该 JavaScript 脚本设置为异步加载,通过 async 或 defer 来标记代码。async 和 defer 虽然都是异步的,不过还有一些差异,使用 async 标志的脚本文件一旦加载完成,会立即执行;而使用了 defer 标记的脚本文件,需要在 DOMContentLoaded 事件之前执行。

CSS 是如何影响页面渲染的

我们经常被问到 JS 是如何阻碍页面渲染的,很少被问到 CSS 是如何阻碍页面渲染的。那 CSS 会不会阻碍页面的渲染呢?

很多网上的答案都是CSS 不会阻塞页面渲染。真的是这样吗?

我们来做个试验:

服务端代码

app.get('/css', (req, res) => {setTimeout(() => {fs.readFile(path.join(__dirname, './render.css'), (err, result) => {res.setHeader('Content-Type', 'text/css')res.setHeader('Access-Control-Allow-Origin', 'http://localhost:52330/')res.end(result)})}, 3000)
})

前端代码:

<!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" /><link rel="stylesheet" href="http://localhost:8001/css" /><title>Document</title></head><body><div id="app">test</div><div id="app1">124143</div></body>
</html>

我们发现页面需要等3s之后才会有内容显示。这说明 CSS 文件会阻碍页面的渲染。

为什么CSS会阻碍页面的渲染呢?

这就需要从渲染流水线的角度来回答这个问题。首先看这段代码,它分别由 CSS 文件和 HTML 文件构成:

<html><head><link href="theme.css" rel="stylesheet" /></head><body><div>com</div></body>
</html>

来分析下打开这段 HTML 文件时的渲染流水线,可以先参考下面这张渲染流水线示意图:

首先是发起主页面的请求,这个发起请求方可能是渲染进程,也有可能是浏览器进程,发起的请求被送到网络进程中去执行。网络进程接收到返回的 HTML 数据之后,将其发送给渲染进程,渲染进程会解析 HTML 数据并构建 DOM。这里你需要特别注意下,请求 HTML 数据和构建 DOM 中间有一段空闲时间,这个空闲时间有可能成为页面渲染的瓶颈

当渲染进程接收到 HTML 文件字节流时,会先开启一个预解析线程,如果遇到 JavaScript 文件或者 CSS 文件,那么预解析线程会提前下载这些数据。

预解析线程会解析出来一个外部的 theme.css 文件,并发起 theme.css 的下载。这里也有一个空闲时间需要你注意一下,就是在 DOM 构建结束之后、theme.css 文件还未下载完成的这段时间内,渲染流水线无事可做,因为下一步是合成布局树,而合成布局树需要 CSSOM 和 DOM,所以这里需要等待 CSS 加载结束并解析成 CSSOM

那渲染流水线为什么需要 CSSOM 呢?

和 HTML 一样,渲染引擎也是无法直接理解 CSS 文件内容的,所以需要将其解析成渲染引擎能够理解的结构,这个结构就是 CSSOM。和 DOM 一样,CSSOM 也具有两个作用,第一个是提供给 JavaScript 操作样式表的能力,第二个是为布局树的合成提供基础的样式信息。这个 CSSOM 体现在 DOM 中就是document.styleSheets

等 DOM 和 CSSOM 都构建好之后,渲染引擎就会构造布局树。布局树的结构基本上就是复制 DOM 树的结构,不同之处在于 DOM 树中那些不需要显示的元素会被过滤掉,如 display:none 属性的元素、head 标签、script 标签等。

复制好基本的布局树结构之后,渲染引擎会为对应的 DOM 元素选择对应的样式信息,这个过程就是样式计算。样式计算完成之后,渲染引擎还需要计算布局树中每个元素对应的几何位置,这个过程就是计算布局

通过样式计算和计算布局就完成了最终布局树的构建。有了布局树才能进行页面的渲染。

所以 CSS 文件是肯定会阻碍页面的渲染的。

我们再来看看稍微复杂一点的场景,还是看下面这段 HTML 代码:

<html><head><style src="theme.css"></style></head><body><div>1</div><script> let div1 = document.getElementsByTagName('div')[0]div1.innerText = 'time.geekbang' //需要DOMdiv1.style.color = 'red' //需要CSSOM </script><div>test</div></body>
</html>

该示例中,JavaScript 代码出现了 div1.style.color = ‘red’ 的语句,它是用来操纵 CSSOM 的,所以在执行 JavaScript 之前,需要先解析 JavaScript 语句之上所有的 CSS 样式

所以如果代码里引用了外部的 CSS 文件,那么在执行 JavaScript 之前,还需要等待外部的 CSS 文件下载完成,并解析生成 CSSOM 对象之后,才能执行 JavaScript 脚本

而 JavaScript 引擎在解析 JavaScript 之前,是不知道 JavaScript 是否操纵了 CSSOM 的,所以渲染引擎在遇到 JavaScript 脚本时,不管该脚本是否操纵了 CSSOM,都会执行 CSS 文件下载,解析操作,再执行 JavaScript 脚本。

所以说, CSS 在部分情况下也会阻塞 DOM 的生成。

总结下:CSS 不管在有没有脚本的情况下都是会阻碍页面的渲染的。如果没有脚本,因为渲染需要布局树,布局树需要CSS,所以它会影响页面渲染。如果有脚本,因为脚本可能操作CSSOM,所以需要等CSS文件下载完成后才能执行脚本,它其实是阻碍了脚本的执行,间接的阻碍了DOM的解析。

通过对上面的了解,下面来看看这段代码的渲染流程:

<html><head><link href="theme.css" rel="stylesheet" /></head><body><div>geekbang com</div><script src="foo.js"></script><div>geekbang com</div></body>
</html>

在接收到 HTML 数据之后的预解析过程中,HTML 预解析器识别出来了有 CSS 文件和 JavaScript 文件需要下载,然后就同时发起这两个文件的下载请求,需要注意的是,这两个文件的下载过程是重叠的,所以下载时间按照最久的那个文件来算。

不管 CSS 文件和 JavaScript 文件谁先到达,都要先等到 CSS 文件下载完成并生成 CSSOM,然后再执行 JavaScript 脚本,最后再继续构建 DOM,构建布局树,绘制页面。

影响页面展示的因素以及优化策略

那么接下来我们就来看看从发起 URL 请求开始,到首次显示页面的内容,在视觉上经历的三个阶段。

  • 第一个阶段,等请求发出去之后,到提交数据阶段,这时页面展示出来的还是之前页面的内容,参考详解在浏览器中从输入URL到页面展示的过程。
  • 第二个阶段,提交数据之后渲染进程会创建一个空白页面,我们通常把这段时间称为解析白屏,并等待 CSS 文件和 JavaScript 文件的加载完成,生成 CSSOM 和 DOM,然后合成布局树,最后还要经过一系列的步骤准备首次渲染。
  • 第三个阶段,等首次渲染完成之后,就开始进入完整页面的生成阶段了,然后页面会一点点被绘制出来。

影响第一个阶段的因素主要是网络或者是服务器处理这块儿。现在重点关注第二个阶段,这个阶段的主要问题是白屏时间,如果白屏时间过久,就会影响到用户体验。为了缩短白屏时间,我们来挨个分析这个阶段的主要任务,包括了解析 HTML、下载 CSS、下载 JavaScript、生成 CSSOM、执行 JavaScript、生成布局树、绘制页面一系列操作。

通常情况下的瓶颈主要体现在下载 CSS 文件、下载 JavaScript 文件和执行 JavaScript。所以要想缩短白屏时长,可以有以下策略:

  • 通过内联 JavaScript、内联 CSS 来移除这两种类型的文件下载,这样获取到 HTML 文件之后就可以直接开始渲染流程了。
  • 但并不是所有的场合都适合内联,那么还可以尽量减少文件大小,比如通过 webpack 等工具移除一些不必要的注释,并压缩 JavaScript 文件。
  • 还可以将一些不需要在解析 HTML 阶段使用的 JavaScript 标记上 async 或者 defer。
  • 对于大的 CSS 文件,可以通过媒体查询属性,将其拆分为多个不同用途的 CSS 文件,这样只有在特定的场景下才会加载特定的 CSS 文件。

最后

整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。

有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享

部分文档展示:



文章篇幅有限,后面的内容就不一一展示了

有需要的小伙伴,可以点下方卡片免费领取

JS 和 CSS 是如何影响页面渲染的?相关推荐

  1. 【Web技术】1005- 关于 JS 与 CSS 是否阻塞 DOM 的渲染和解析

    最近系统梳理HTML5所有涉及到的标签时,梳理至<link>和<script>标签时,碰巧想到一个困扰很久的问题,即一般把<script>放在<body> ...

  2. 最全面梳理 JS 运行机制解析与浏览器页面渲染的核心流程

    最近这几年,云计算的普及和 HTML5 技术的快速发展,越来越多的应用转向了浏览器 / 服务器(B/S)架构,这种改变让浏览器的重要性与日俱增,视频.音频.游戏几大核心场景也都在逐渐往 Web 使用场 ...

  3. css 注入,electron程序,如何在主进程远程页面中注入js及css?

    本博客不欢迎:各种镜像采集行为,请尊重知识产权法律法规.大家都是程序员,不要闹得不开心. 每日一篇的苏南大叔写代码教程,又来了.在本文中,苏南大叔描述的是,在electron程序加载远程页面的时候,如 ...

  4. 基于maven maven-replacer-plugin 插件对JS,CSS统一加版本号

    2019独角兽企业重金招聘Python工程师标准>>> 基于maven对JS,CSS统一加版本号 在写WEB应用的时候,每次对JS/CSS文件做的修改,对于用户来说,都很郁闷:一不注 ...

  5. 5渲染判断_Vue页面渲染中key的应用实例教程

    引言 在前端项目开发过程中,el-table展示的结果列使用组件形式引入,其中某些字段通过:formatter方法转码,结果栏位的字段显示/隐藏控制也使用组件形式引入,前端在控制字段显示属性时,发现码 ...

  6. 将CSS放头部,JS放底部,可以提高页面的性能的原因

    css不阻止dom的解析 js阻止dom的解析 css js都会阻止dom的渲染 原因: js有可能影响dom的解析,比如在js里面新增dom等这些操作 css不能影响dom的解析 而 dom的渲染 ...

  7. html中隐藏内容蜘蛛会抓取吗,蜘蛛会抓取识别JS、CSS、JSON,对SEO有什么影响

    这是一个存在多年.经常出现但又从来没有标准解决办法的问题:搜索引擎爬虫(尤其是百度)抓取JS.CSS.JSON文件,robots屏蔽依然抓取的情况. 这就引出了几个问题: 1.爬虫抓取JS.CSS是干 ...

  8. Web项目中前端页面引用外部Js和Css的路径问题

    公众号:南宫一梦 Web项目中前端页面引用外部Js和Css的路径问题 一般我们在做Web项目时,通常会将多个页面引入的公共js和css文件抽取出来,单独写成一个公共文件,以期方便各个页面单独引入,达到 ...

  9. 仅使用CSS提高页面渲染速度

    用户在访问一个Web网站(页面)或应用时,总是希望它的加载速度快,功能流畅.如果过于慢,用户就很有可能失去耐心而离开你的Web网站或应用.作为开发人员,给自己应用提供更快的访问速度,提供很好的用户体验 ...

最新文章

  1. java 时间戳 与时间的转换
  2. php视频录制插件,Chrome浏览器录屏扩展插件
  3. 部署项目到阿里云服务器上遇到的问题
  4. CRMEB删除公众号首页logo动画
  5. 前端学习(1961)vue之电商管理系统电商系统之调用api获取数据
  6. 第一百一十三期:去伪存真,区块链应用到底能解决什么实际问题?
  7. 2019,转行人工智能?机会来了!
  8. c语言api函数写病毒,C语言病毒代码,及写病毒简单介绍
  9. 微软 azure_有关Microsoft Azure技术的简介和常见问题解答
  10. Win7主题制作修改教程
  11. 遗传算法详解与MATLAB实现
  12. wireshark抓取网络数据包
  13. pycharm如何添加桌面图标_桌面图标全变成pycharm了怎么办?
  14. 测试行业4年经验,面试进了阿里,二个月后我果断选择裸辞...
  15. IOS开发学习--(3)摇骰子APP
  16. Oracle 游标详解(cursor)
  17. HTML - 03 网页元素的属性
  18. Python3.6爬取前程无忧
  19. ARM汇编指令以及伪指令
  20. 邮箱smtp服务器及端口收集

热门文章

  1. csdn怎么让代码变得好看_是什么让游戏变得更好
  2. 进程池创建多进程下载网页
  3. 计算广告及搜索广告简介
  4. 96年的小同事找了一份高薪工作
  5. (资源)诸葛学堂窦神归来第二季:中国古代文学文化
  6. 元宇宙(metaverse)的认知记录
  7. 12V将路由器网口烧了
  8. 娱乐头条-03spider
  9. BIST(build_in selftest)介绍
  10. 轻开商贸企业入门级电子商务 B2C网站公共版