设为首页收藏本站心情墙手机版 今天是: 2024-04-27    美好的一天,从现在开始
天气与日历 切换到宽版

 找回密码
 立即注册
搜索
查看: 496|回复: 0

[html] html5页面加载速度优化(详解dom css js加载优化)

[复制链接]
  • 打卡等级:LV6

452

主题

30

回帖

24万

积分

管理员

积分
247191

突出贡献荣誉管理论坛元老本科学士学位拥有劳力士宇宙计型迪通拿系列m116515ln-0059拥有欧米茄星座系列131.23.41.21.03.001拥有梅赛德斯-奔驰EQS 580 4MATIC拥有宝马M8四门轿跑车 雷霆版

QQ

皮卡丘 Lv:40
发表于 2023-1-1 10:16:45 | 显示全部楼层 |阅读模式 IP:北京

浏览器通过关键渲染路径概念这个过程对 HTML、CSS、JavaScript 等资源文件进行解析,然后组织渲染出最终的页面。本章将以此为基础,对渲染过程进行更深入的讨论,不仅包括打开一个网站的首次渲染,还有用户与页面进行交互后导致页面更改的渲染,即所谓的重绘与重排。其中除了对渲染过程的充分介绍,更重要的是对提升渲染过程性能的优化手段的探讨。

浏览器从获取 HTML 到最终在屏幕上显示内容需要完成以下步骤:


  • 处理 HTML 标记并构建 DOM 树。
  • 处理 CSS 标记并构建 CSSOM 树。
  • 将 DOM 与 CSSOM 合并成一个 render tree。
  • 根据渲染树来布局,以计算每个节点的几何信息。
  • 将各个节点绘制到屏幕上。


经过以上整个流程我们才能看见屏幕上出现渲染的内容,优化关键渲染路径就是指最大限度缩短执行上述第 1 步至第 5 步耗费的总时间,让用户最快的看到首次渲染的内容。

不但网站页面要快速加载出来,而且运行过程也应更顺畅,在响应用户操作时也要更加及时,比如我们通常使用手机浏览网上商城时,指尖滑动屏幕与页面滚动应很流畅,拒绝卡顿。那么要达到怎样的性能指标,才能满足用户流畅的使用体验呢?

目前大部分设备的屏幕分辨率都在60 fps 左右,也就是每秒屏幕会刷新60次,所以要满足用户的体验期望,就需要浏览器在渲染页面动画或响应用户操作时,每一帧的生成速率尽量接近屏幕的刷新率。若按照60 fps 来算,则留给每一帧画面的时间不到 17 ms,再除去浏览器对资源的一些整理工作,一帧画面的渲染应尽量在10 ms 内完成,如果达不到要求而导致帧率下降,则屏幕上的内容会发生抖动或卡顿。

为了使每一帧页面渲染的开销都能在期望的时间范围内完成,就需要开发者了解渲染过程的每个阶段,以及各阶段中有哪些优化空间是我们力所能及的。经过分析根据开发者对优化渲染过程的控制力度,可以大体将其划分为五个部分:JavaScript 处理、计算样式、页面布局、绘制与合成,下面先简要介绍各部分的功能与作用。

html5页面加载速度优化(dom css js加载优化)

html5页面加载速度优化(dom css js加载优化)


  • JavaScript 处理:前端项目中经常会需要响应用户操作,通过 JavaScript 对数据集进行计算、操作 DOM 元素,并展示动画等视觉效果。当然对于动画的实现,除了 JavaScript,也可以考虑使用如 CSS Animations、Transitions 等技术。
  • 计算样式:在解析 CSS 文件后,浏览器需要根据各种选择器去匹配所要应用 CSS 规则的元素节点,然后计算出每个元素的最终样式。
  • 页面布局:指的是浏览器在计算完成样式后,会对每个元素尺寸大小和屏幕位置进行计算。由于每个元素都可能会受到其他元素的影响,并且位于 DOM 树形结构中的子节点元素,总会受到父级元素修改的影响,所以页面布局的计算会经常发生。
  • 绘制:在页面布局确定后,接下来便可以绘制元素的可视内容,包括颜色、边框、阴影及文本和图像。
  • 合成:通常由于页面中的不同部分可能被绘制在多个图层上,所以在绘制完成后需要将多个图层按照正确的顺序在屏幕上合成,以便最终正确地渲染出来。

这个过程中的每一阶段都有可能产生卡顿,本章后续内容将会对各阶段所涉及的性能优化进行详细介绍。这里值得说明的是,并非对于每一帧画面都会经历这五个部分。比如仅修改与绘制相关的属性(文字颜色、背景图片或边缘阴影等),而未对页面布局产生任何修改,那么在计算样式阶段完成后,便会跳过页面布局直接执行绘制。


关键渲染路径优化


CSSOM 会阻塞渲染,只有当 CSSOM 构建完毕后才会进入下一个阶段构建渲染树。


通常情况下 DOM 和 CSSOM 是并行构建的,但是当浏览器遇到一个script标签时,DOM 构建将暂停,直至脚本完成执行。但由于 JavaScript 可以修改 CSSOM,所以需要等 CSSOM 构建完毕后再执行 JS。


经过以上整个流程我们才能看见屏幕上出现渲染的内容,优化关键渲染路径就是指最大限度缩短执行上述第 1 步至第 5 步耗费的总时间,让用户最快的看到首次渲染的内容。


能阻塞网页首次渲染的资源称为关键资源,为尽快完成首次渲染,我们需要最大限度减小以下三种可变因素:


  • 关键资源的数量:可能阻止网页首次渲染的资源


例如 JavaScript、CSS 都是可以阻塞关键渲染路径的资源,这些资源越少,浏览器的工作量就越小,对 CPU 以及其他资源的占用也就越少。

  • 关键路径长度:获取所有关键资源所需的往返次数或总时间


关键路径长度受所有关键资源与其字节大小之间依赖关系图的影响:某些资源只能在上一资源处理完毕之后才能开始下载,并且资源越大,下载所需的往返次数就越多。

  • 关键字节的数量:实现网页首次渲染所需的总字节数


浏览器需要下载的关键字节越少,处理内容并让其出现在屏幕上的速度就越快。要减少字节数,我们可以减少资源数(将它们删除或设为非关键资源),此外还要压缩和优化各项资源,确保最大限度减小传送大小。


优化 DOM


在关键渲染路径中,构建渲染树(Render Tree)的第一步是构建 DOM,所以我们先讨论如何让构建 DOM 的速度变得更快。


HTML 文件的尺寸应该尽可能的小,目的是为了让客户端尽可能早的接收到完整的 HTML。通常 HTML 中有很多冗余的字符,例如:JS 注释、CSS 注释、HTML 注释、空格、换行。更糟糕的情况是我见过很多生产环境中的 HTML 里面包含了很多废弃代码,这可能是因为随着时间的推移,项目越来越大,由于种种原因从历史遗留下来的问题,不过不管怎么说,这都是很糟糕的。对于生产环境的 HTML 来说,应该删除一切无用的代码,尽可能保证 HTML 文件精简。


总结起来有三种方式可以优化 HTML:缩小文件的尺寸(Minify)、使用gzip压缩(Compress)、使用缓存(HTTP Cache)。


缩小文件的尺寸(Minify)会删除注释、空格与换行等无用的文本。


本质上,优化 DOM 其实是在尽可能的减小关键路径的长度与关键字节的数量。


优化 CSSOM

CSS 是构建渲染树的必备元素,首次构建网页时,JavaScript 常常受阻于 CSS。确保将任何非必需的 CSS 都标记为非关键资源(例如打印和其他媒体查询),并应确保尽可能减少关键 CSS 的数量,以及尽可能缩短传送时间。


阻塞渲染的 CSS

除了上面提到的优化策略,CSS 还有一个可以影响性能的因素是:CSS 会阻塞关键渲染路径。


下面示例中的样式文件会阻塞 DOM 树的构建,阻塞 3 秒钟:

  1. // app.js
  2. const express = require('express')
  3. const fsPromises = require('fs').promises

  4. const app = express()

  5. app.get('/', async (req, res) => {
  6.   const data = await fsPromises.readFile('./index.html')
  7.   res.end(data)
  8. })

  9. app.get('/style.css', async (req, res) => {
  10.   const data = await fsPromises.readFile('./style.css')
  11.   setTimeout(() => {
  12.     res.end(data)
  13.   }, 3000)
  14. })

  15. app.listen(3000, () => {
  16.   console.log('http://localhost:3000')
  17. })

复制代码
  1. <!-- index.html -->
  2. <!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5.   <title>Critical Path</title>
  6.   <link href="style.css" rel="stylesheet">
  7. </head>
  8. <body>
  9.   <h1>Hello <span>web performance</span> students!</h1>
  10. </body>
  11. </html>

复制代码



CSS 是关键资源,它会阻塞关键渲染路径也并不奇怪,但通常并不是所有的 CSS 资源都那么的『关键』。


举个例子:一些响应式 CSS 只在屏幕宽度符合条件时才会生效,还有一些 CSS 只在打印页面时才生效。这些 CSS 在不符合条件时,是不会生效的,所以我们为什么要让浏览器等待我们并不需要的 CSS 资源呢?


针对这种情况,我们应该让这些非关键的 CSS 资源不阻塞渲染。

  1. <!-- 阻塞渲染 -->
  2. <link href="style.css" rel="stylesheet">

  3. <!-- 非阻塞的加载 CSS:打印时使用 -->
  4. <link href="print.css" rel="stylesheet" media="print">

  5. <!-- 可变阻塞加载:满足分辨率时使用 -->
  6. <link href="other.css" rel="stylesheet" media="(min-width: 40em)">

  7. <!-- 可变阻塞加载:满足设备方向时使用 -->
  8. <link href="portrait.css" rel="stylesheet" media="orientation:portrait">
复制代码



  • 第一个声明阻塞渲染,适用于所有情况。
  • 第二个声明只在打印网页时应用,因此网页首次在浏览器中加载时,它不会阻塞渲染。
  • 第三个声明提供由浏览器执行的“媒体查询”:符合条件时,浏览器将阻塞渲染,直至样式表下载并处理完毕。
  • 最后一个声明具有动态媒体查询,将在网页加载时计算。根据网页加载时设备的方向,portrait.css 可能阻塞渲染,也可能不阻塞渲染。
  • 最后,请注意“阻塞渲染”仅是指浏览器是否需要暂停网页的首次渲染,直至该资源准备就绪。无论哪一种情况,浏览器仍会下载 CSS 资产,只不过不阻塞渲染的资源优先级较低罢了。


为获得最佳性能,您可能会考虑将关键 CSS 直接内联到 HTML 文档内。这样做不会增加关键路径中的往返次数,并且如果实现得当,在只有 HTML 是阻塞渲染的资源时,可实现“一次往返”关键路径长度。


避免在 CSS 中使用 @import

大家应该都知道要避免使用 @import 加载 CSS,实际工作中我们也不会这样去加载 CSS,但这到底是为什么呢?


这是因为使用 @import 加载 CSS 会增加额外的关键路径长度。举个例子,增加一个样式文件:

  1. // app.js
  2. app.get('/main.css', async (req, res) => {
  3.   const data = await fsPromises.readFile('./main.css')
  4.   setTimeout(() => {
  5.     res.end(data)
  6.   }, 4000)
  7. })
复制代码
  1. /* style.css */
  2. @import url(/main.css);
  3. body {
  4.   background-color: pink;
  5. }

复制代码



现在 style.css 中使用 @import 加载了 main.css,从 F12 network 面板可以看到两个 CSS 资源是串行加载的,前一个 CSS 加载完后再去下载使用 @import 导入的 CSS 资源。这无疑会导致加载资源的总时间变长。现在页面的总加载时间至少是 7 秒(3 + 4)。



html5页面加载速度优化(dom css js加载优化)

html5页面加载速度优化(dom css js加载优化)


如果都是用 link 加载:

  1. <link href="style.css" rel="stylesheet">
  2. <link href="main.css" rel="stylesheet">
复制代码
  1. /* @import url(/main.css); */
  2. body {
  3.   background-color: pink;
  4. }

复制代码


两个资源现在是并行下载,页面加载总时间大大缩短到 4 秒多一点。



html5页面加载速度优化(dom css js加载优化)

html5页面加载速度优化(dom css js加载优化)


所以避免使用 @import 是为了降低关键路径的长度。


优化 JavaScript 的使用

常用的解决方案:


  • 控制文件大小:所有文本资源都应该让文件尽可能的小,JavaScript 也不例外,它也需要删除未使用的代码、缩小文件的尺寸(Minify)、使用 gzip 压缩(Compress)、使用缓存(HTTP Cache)。
  • 异步加载 JavaScript
  • 避免同步请求
  • 延迟解析 JavaScript
  • 避免运行时间长的 JavaScript
  • 使用 defer 延迟加载 JavaScript

与 CSS 资源相似,JavaScript 资源也是关键资源,JavaScript 资源会阻塞 DOM 的构建。并且 JavaScript 会被 CSS 文件所阻塞。


当浏览器加载 HTML 时遇到 <script>...</script> 标签,浏览器就不能继续构建 DOM。它必须立刻执行此脚本。对于外部脚本 <script src="..."></script> 也是一样的:浏览器必须等脚本下载完,并执行结束,之后才能继续处理剩余的页面。


这会导致两个重要的问题:


  • 脚本不能访问到位于它们下面的 DOM 元素,因此,脚本无法给它们添加处理程序等。
  • 如果页面顶部有一个笨重的脚本,它会“阻塞页面”。在该脚本下载并执行结束前,用户都不能看到页面内容


示例添加脚本资源:

  1. // script.js
  2. console.log('hello script')
复制代码
  1. // script2.js
  2. console.log('hello script2')
复制代码
  1. // app.js
  2. app.get('/script.js', async (req, res) => {
  3.   const data = await fsPromises.readFile('./script.js')
  4.   setTimeout(() => {
  5.     res.end(data)
  6.   }, 4000)
  7. })

  8. app.get('/script2.js', async (req, res) => {
  9.   const data = await fsPromises.readFile('./script2.js')
  10.   setTimeout(() => {
  11.     res.end(data)
  12.   }, 2000)
  13. })
复制代码
  1. <!-- index.html -->
  2. <p>...content before script...</p>

  3. <script src="script.js"></script>

  4. <!-- 在脚本加载之前,这是不可见的 -->
  5. <p>...content after script...</p>
复制代码



这里有一些解决办法。例如,我们可以把脚本放在页面底部。此时,它可以访问到它上面的元素,并且不会阻塞页面显示内容

  1. <body>
  2.   ...所有内容都在脚本之前...

  3.   <script src="script.js"></script>
  4. </body>
复制代码



但是这种解决方案远非完美。例如,浏览器只有在下载了完整的 HTML 文档之后才会注意到该脚本(并且可以开始下载它)。对于长的 HTML 文档来说,这样可能会造成明显的延迟。


这对于使用高速连接的人来说,这不值一提,他们不会感受到这种延迟。但是这个世界上仍然有很多地区的人们所使用的网络速度很慢,并且使用的是远非完美的移动互联网连接。


幸运的是,这里有两个 <script> 特性(attribute)可以为我们解决这个问题:defer 和 async。


defer 特性告诉浏览器不要等待脚本。相反,浏览器将继续处理 HTML,构建 DOM。脚本会“在后台”下载,然后等 DOM 构建完成后,脚本才会执行。


这是与上面那个相同的示例,但是带有 defer 特性:

  1. <p>...content before script...</p>

  2. <script defer src="script.js"></script>

  3. <!-- 立即可见 -->
  4. <p>...content after script...</p>
复制代码


换句话说:


  • 具有 defer 特性的脚本不会阻塞页面。
  • 具有 defer 特性的脚本总是要等到 DOM 解析完毕,但在 DOMContentLoaded 事件之前执行。

下面这个示例演示了上面所说的第二句话:

  1. <p>...content before scripts...</p>

  2. <script>
  3.   document.addEventListener('DOMContentLoaded', () => alert("DOM ready after defer!"));
  4. </script>

  5. <script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>

  6. <p>...content after scripts...</p>
复制代码


  • 页面内容立即显示。
  • DOMContentLoaded 事件处理程序等待具有 defer 特性的脚本执行完成。它仅在脚本下载且执行结束后才会被触发。

具有 defer 特性的脚本保持其相对顺序,就像常规脚本一样。


修改示例,加载两个具有 defer 特性的脚本:script.js 在前,script2.js 在后。


  1. <script defer src="script.js"></script>
  2. <script defer src="script2.js"></script>
复制代码


浏览器扫描页面寻找脚本,然后并行下载它们,以提高性能。因此,在上面的示例中,两个脚本是并行下载的。script2.js 可能会先下载完成。



……但是,defer 特性除了告诉浏览器“不要阻塞页面”之外,还可以确保脚本执行的相对顺序。因此,即使 small.js 先加载完成,它也需要等到 long.js 执行结束才会被执行。



当我们需要先加载 JavaScript 库,然后再加载依赖于它的脚本时,这可能会很有用。


注意:defer 特性仅适用于外部脚本,如果 <script> 脚本没有 src,则会忽略 defer 特性。


使用 async 延迟加载 JavaScript

async 特性与 defer 有些类似。它也能够让脚本不阻塞页面。但是,在行为上二者有着重要的区别。


async 特性意味着脚本是完全独立的:


  • 浏览器不会因 async 脚本而阻塞(与 defer 类似)。
  • 其他脚本不会等待 async 脚本加载完成,同样,async 脚本也不会等待其他脚本。
  • DOMContentLoaded 和 async 脚本不会彼此等待:
        DOMContentLoaded 可能会发生在 async 脚本之前(如果 async 脚本在页面完成后才加载完成)

        DOMContentLoaded 也可能发生在 async 脚本之后(如果 async 脚本很短,或者是从 HTTP 缓存中加载的)

换句话说,async 脚本会在后台加载,并在加载就绪时运行。DOM 和其他脚本不会等待它们,它们也不会等待其它的东西。async 脚本就是一个会在加载完成时执行的完全独立的脚本。


下面将 defer 的示例修改为 async,它们不会等待对方。先加载完成的(可能是 script2.js)先执行:

  1. <p>...content before script...</p>

  2. <script>
  3.   document.addEventListener('DOMContentLoaded', () => alert("DOM ready!"));
  4. </script>

  5. <script async src="script.js"></script>
  6. <script async src="script2.js"></script>

  7. <p>...content after script...</p>
复制代码


页面内容立刻显示出来:加载写有 async 的脚本不会阻塞页面渲染。

DOMContentLoaded 可能在 async 之前或之后触发,不能保证谁先谁后。

较快的脚本 script2.js 排在第二位,但可能会比 script.js 这个脚本先加载完成,所以 script2.js 会先执行。虽然,可能是 scrip.js 先加载完成,如果它被缓存了的话,那么它就会先执行。换句话说,异步脚本以“加载优先”的顺序执行。

当我们将独立的第三方脚本集成到页面时,此时采用异步加载方式是非常棒的:计数器,广告等,因为它们不依赖于我们的脚本,我们的脚本也不应该等待它们。


preload/prefetch 预加载资源

defer 和 async 都是解决了因解析到了 js 资源进行加载导致的阻塞页面渲染的问题。


可以对 js 资源进行预加载(提前下载并缓存,但不执行)提高加载速度。


<link> 元素的 rel 属性提供了 preload 和 prefetch 两个值,可以让浏览器利用空闲时间提前下载和缓存资源,优化用户体验:


  • preload:预加载当前页面用到的资源
  • prefetch:预加载将来可能用到的资源

  1. <link rel="preload" href="script.js">
  2. <link rel="prefetch" href="script2.js">
复制代码


**注意:**预加载请求也会遵循缓存策略,如果缓存命中则会从缓存中获取资源。


preload 和 prefetch 的区别

  • preload 用于预加载当前页面用到的资源。

当浏览器解析到 preload 会在当前页面查找是否需要加载这个资源,如果确实需要则会进行预加载,如果没有用到则不会发起请求。

当浏览器解析到 <script> 时,如果预加载已经完成,则不会重复发起请求下载;如果预加载没完成,则会等待下载完成再执行脚本,但下载过程仍不会阻塞页面。

  • prefetch 用于预加载非当前页面可能用到的资源

当浏览器解析到 prefetch 不会关心当前页面是否使用了这个资源,直接进行预加载。

当浏览器解析到 <script> 时,也发起请求为当前页面加载资源,不论是否使用了 prefetch。

所以如果 prefetch 了当前页面需要的资源,则会造成重复加载。

如果为一个资源同时使用了 prefetch 和 preload 也会造成重复加载。


preload 最大的作用就是将下载与执行分离,并将下载的优先级提到一个很高的底部,再由开发者控制资源执行的位置。


例如可以将非首屏渲染所需的 CSS 样式抽离出来作为一个样式表引入,配置 preload,这样即便 HTML 解析到这个文件也不会阻塞渲染。


preload 常用场景

  • CSS:预加载非首屏渲染的样式(如超出屏幕的部分),不会阻塞渲染
  • JS:预加载脚本文件,使用 <script> 控制执行位置
  • FONT:预加载字体文件,避免在解析 CSS 中 @font-face 的 src 时才去加载

注意:跨域资源配置 preload 需要携带 crossorigin,而 @font-face 中加载字体默认发起的是跨域请求,所以字体文件预加载必须配置 crossorigin,否则无法命中缓存,造成重复请求。

  • 动态生成 preload <link> 标签

可以为隐藏在 CSS 和 JS 中的资源动态生成 preload <link> 标签,这样可以在页面中提前加载,等到需要时再使用。

  • 配合 meida 媒体查询条件实现响应式加载资源

例如针对移动端和 PC 端加载不同图片


总结

关键渲染路径是浏览器将 HTML、CSS、JavaScript 转换为屏幕上所呈现的实际像素的具体步骤。而优化关键渲染路径可以提高网页的呈现速度。


以上都是如何优化 DOM、CSSOM 以及 JavaScript,因为通常在关键渲染路径中,这些步骤的性能最差。这些步骤是导致首屏渲染速度慢的主要原因。



急躁,是因为经历不够,轻浮,是因为磨练不够,烦乱,是因为思路不清,压力,是因为格局不够,恐惧,是因为假想太多,在这个薄凉的世界,自己不强大,一切都是浮云 ...
懒得打字嘛,点击右侧快捷回复 【乱回复纯数字纯字母将禁言】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|社区规范|绵羊优创 ( 京ICP备19037745号-2 )|网站地图

公安备案京公网安备11011502037529号

GMT+8, 2024-4-27 11:11 , Processed in 1.264004 second(s), 20 queries , MemCache On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表