共计 7551 个字符,预计需要花费 19 分钟才能阅读完成。
作者:京东科技 孙凯
一、前言
置信很多前端开发者在做我的项目时同时也都做过页面性能优化,这不单是前端的必备职业技能,也是考验一个前端根底是否扎实的考点,而性能指标也通常是每一个开发者的绩效之一。尤其马上靠近年关,页面白屏工夫是否过长、首屏加载速度是否达标、动画是否能流畅运行,诸如此类对于性能更具体的指标和感触,很可能也是决定着年底你能拿多少年终奖回家过年的 晴雨表。
对于性能优化,咱们个别从以下四个方面思考:
- 开发时性能优化
- 编译时性能优化
- 加载时性能优化
- 运行时性能优化
而本文将从第三个方面开展,讲一讲哪些因素将影响到页面加载总时长,谈到总时长,那总是防止不了要谈及 window.onload
,这不然而本文的重点,也是常见 页面性能监控工具中必要的 API 之一,如果你对本人页面加载的总时长不称心,欢送读完本文后在评论区交换。
二、对于 window.onload
这个挂载到 window
上的办法,是我刚接触前端时就把握的技能,我记得尤为粗浅,过后老师说,“对于初学者,只有在这个办法里写逻辑,肯定没错儿,它是整个文档加载结束后执行的生命周期函数”,于是从那之后,简直所有的练习 demo,我都写在这里,也的确没出过错。
在 MDN
上,对于 onload
的解释是这样的:load 事件在整个页面及所有依赖资源如样式表和图片都已实现加载时触发。它与 DOMContentLoaded
不同,后者只有页面 DOM 加载实现就触发,无需期待依赖资源的加载。该事件不可勾销,也不会冒泡。
起初随着前端常识的一直裁减,这个办法起初因为有了“更先进”的 DOMContentLoaded
,在我的代码里而逐步被代替了,目前除了一些极其非凡的状况,否则我简直很难用到window.onload
这个 API,直到意识到它影响到页面加载的整体时长指标,我才又一次拾起来它。
三、哪些因素会影响 window.onload
本章节次要会通过几个罕用的业务场景开展形容,然而有个前提,就是如何精确记录各种类型资源加载耗时对页面整体加载的影响,为此,有必要先介绍一下前提。
为了精确形容资源加载耗时,我在本地环境启动了一个用于资源申请的 node
服务,所有的资源都会从这个服务中获取,之所以不必近程服务器资源的有次要起因是,应用本地服务的资源能够在拜访的资源链接中设置延迟时间,如拜访脚本资源http://localhost:3010/index.js?delay=300
,因链接中存在delay=300
,即可使资源在 300 毫秒后返回,这样即可精确管制每个资源加载的工夫。
以下是 node
资源申请服务提早相干代码,仅仅是一个中间件:
const express = require("express")
const app = express()
app.use(function (req, res, next) {Number(req.query.delay) > 0
? setTimeout(next, req.query.delay)
: next()})
-
场景一 :应用 async 异步加载脚本场景对 onload 的影响
示例代码:<!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>test</title> <!-- 申请时长为 1 秒的 js 资源 --> <script src="http://localhost:3010/index.js?delay=1000" async></script> </head> <body> </body> </html>
浏览器体现如下:
通过上图能够看到,瀑布图中深蓝色竖线示意触发了
DOMContentLoaded
事件,而红色竖线示意触发了window.onload
事件(下文中无非凡状况,不会再进行非凡标识),由图能够得悉应用了 async 属性进行脚本的异步加载,仍会影响页面加载总体时长。 -
场景二 :应用 defer 异步加载脚本场景对 onload 的影响
示例代码:<!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>test</title> <!-- 申请时长为 1 秒的 js 资源 --> <script src="http://localhost:3010/index.js?delay=1000" defer></script> </head> <body> </body> </html>
浏览器体现如下:
由图能够得悉应用了 defer 属性进行脚本的异步加载,除了失常的在
DOMContentLoaded
之后触发脚本执行,也影响页面加载总体时长。 -
场景三 :异步脚本中再次加载脚本,也就是常见的动静加载脚本、款式资源的状况
html 代码放弃不变,index.js
内示例代码:const script = document.createElement('script') // 申请时长为 0.6 秒的 js 资源 script.src = 'http://localhost:3010/index2.js?delay=600' script.onload = () => {console.log('js 2 异步加载结束') } document.body.appendChild(script)
后果如下:
从瀑布图能够看出,资源的间断加载,导致了 onload 事件整体延后了,这也是咱们再页面中十分常见的一种操作,通常懒加载一些不重要或者首屏外的资源,其实这样也会导致页面整体指标的降落。
不过值得强调的一点是,这里有个有意思的中央,如果咱们把上述代码进行革新,删除最初一行的
document.body.appendChild(script)
,发现 index2 的资源申请并没有收回,也就是说, 脚本元素不向页面中插入,脚本的申请是不会收回的,然而也会有反例,这个咱们上面再说。在本示例中,起初我又把脚本申请换成了 css 申请,后果是统一的。
-
场景四:图片的懒加载 / 预加载
html 放弃不变,index.js 用于加载图片,内容如下:const img = document.createElement('img') // 申请时长为 0.5 秒的图片资源 img.src = 'http://localhost:3010/index.png?delay=500' document.body.appendChild(img)
后果示意:
体现是与场景三一样的,这个不再多说,然而有意思的来了,不一样的是,通过测试发现,哪怕删除最初一行代码:
document.body.appendChild(img)
,不向页面中插入元素,图片也会发出请求,也同样缩短了页面加载时长,所以局部同学就要留神了,这是一把双刃剑:当你真的须要懒加载图片时,能够少写最初一行插入元素的代码了,然而如果大量的图片加载申请收回,哪怕不向页面插入图片,也真的会拖慢页面的时长。趁着这个场景,再多说一句,一些埋点数据的上报,也正是借着图片有不须要插入 dom 即可发送申请的个性,实现胜利上传的。
-
场景五:一般接口申请
html 放弃不变,index.js 内容如下:// 申请时长为 500 毫秒的申请接口 fetch('http://localhost:3010/api?delay=500')
后果如下图:
能够发现一般接口申请的收回,并不会影响页面加载,然而咱们再把场景弄简单一些,见场景六。
-
场景六:同时加载款式、脚本,脚本加载实现后,外部 http 接口申请,等申请后果返回后,再收回图片申请或批改 dom,这也是更贴近生产环境的实在场景
html 代码:<!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>test</title> <!-- 申请时长为 1.2 秒的 css --> <link rel="stylesheet" href="http://localhost:3010/index.css?delay=1200"> <!-- 申请时长为 0.4 秒的 js --> <script src="http://localhost:3010/index.js?delay=400" async></script> </head> <body> </body> </html>
index.js 代码:
async function getImage () { // 申请时长为 0.5 秒的接口申请 await fetch('http://localhost:3010/api?delay=500') const img = document.createElement('img') // 申请时长为 0.5 秒的图片资源 img.src = 'http://localhost:3010/index.png?delay=500' document.body.appendChild(img) } getImage()
后果图如下:
如图所示,联合场景五记的后果,尽管一般的 api 申请并不会影响页面加载时长,然而因为 api 申请过后,从新申请了图片资源(或大量操作 dom),仍然会导致页面加载工夫变长。这也是咱们日常开发中最常见的场景,页面加载了 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>test</title> </head> <body> <video src="http://localhost:3010/video.mp4?delay=500" controls></video> </body> </html>
后果如图:
对于视频这种多媒体资源的加载比拟有意思,video 标签对于资源的加载是默认开启 preload 的,所以资源会默认进行网络申请(如需敞开,要把 preload 设置为 none),能够看到红色竖线根本处于图中绿色条和蓝色条两头(实际上更偏右一些),图片绿色局部代表资源期待时长,蓝色局部代表资源真正的加载时长,且蓝色加载条在 onload 的竖线右侧,这阐明多媒体的资源的确影响了 onload 时长,然而又没齐全影响,因为设置了 500ms 的提早返回资源,所以 onload 也被提早了 500ms 左右,但一旦视频真正开始下载,这段时长曾经不记录在 onload 的时长中了。
其实这种行为也算正当,毕竟多媒体资源通常很大,占用的带宽也多,如果始终提早 onload,意味着很多依赖 onload 的事件都无奈及时触发。
接下来咱们把这种状况再简单一些,贴近理论的生产场景,通常 video 元素是蕴含封面图 poster 属性的,咱们设置一张提早 1 秒的封面图,看看会产生什么,后果如下:
不出意外,果然封面图影响了整体的加载时长,魔鬼都在细节中,封面图也须要留神优化压缩。
-
场景八 :异步脚本和款式资源一起申请
示例代码:<!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>test</title> <!-- 申请时长为 1 秒的 css --> <link rel="stylesheet" href="http://localhost:3010/index.css?delay=1000"> <!-- 申请时长为 0.5 秒的 js --> <script src="http://localhost:3010/index.js?delay=500" async></script> </head> <body> </body> </html>
浏览器体现如下:
能够看出 css 资源尽管没有阻塞脚本的加载,然而却提早了整体页面加载时长,其中起因是 css 资源的加载会影响 render tree 的生成,导致页面迟迟不能实现渲染。
如果尝试把 async 换成 defer,或者罗唆应用同步的形式加载脚本,后果也是一样,因后果雷同,本处不再举例。 -
场景九 :款式资源先申请,再执行内联脚本逻辑,最初加载异步脚本
咱们把场景八的代码做一个革新,在款式标签和异步脚本标签之间,加上一个只蕴含空格的内联脚本,让咱们看看会产生什么,代码如下:<!DOCTYPE html> <html lang="en"> <head> <script> console.log('页面 js 开始执行') </script> <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>test</title> <!-- 申请时长为 1 秒的 css --> <link rel="stylesheet" href="http://localhost:3010/index.css?delay=2000"> <!-- 此标签仅有一个空格 --> <script> </script> <!-- 申请时长为 0.5 秒的 js --> <script src="http://localhost:3010/index.js?delay=500" async></script> </head> <body> </body> </html>
index.js 中的内容如下:
console.log("脚本 js 开始执行");
后果如下,这是一张 GIF,加载可能有点慢:
这个后果十分有意思,他到底产生了什么呢?
- 脚本申请是 0.5 秒的提早,款式申请是 2 秒
- 脚本资源是 async 的申请,异步收回,应该什么时候加载完什么时候执行
- 然而图中的后果却是期待款式资源加载结束后才执行
** 答案就在那个仅有一个空格的脚本标签中 **,经重复测试,如果把标签换成正文,也会呈现一样的景象,如果是一个齐全空的标签,或者基本没有这个脚本标签,那下方的 index.js 通过 async 异步加载,并不会违反直觉,加载结束后间接执行了,所以这是为什么呢?这其实是因为款式资源下方的 script 尽管仅有一个空格,然而被浏览器认为了它外部可能是蕴含逻辑,肯定概率会存在款式的批改、更新 dom 构造等操作,因为款式资源没有加载完(被提早了 2 秒),导致同步 js(只有一个空格的脚本)的执行被阻塞了,家喻户晓页面的渲染和运行是单线程的,既然后面曾经有了一个未执行实现的 js,所以也导致了前面异步加载的 js 须要在队列中期待。这也就是为什么 async 尽管异步加载了,然而没有在加载后立刻执行的起因。
-
场景十 :字体资源的加载
示例代码:<!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>test</title> <style> @font-face { font-family: font-custom; src: url('http://localhost:3010/font.ttf?delay=500'); } body {font-family: font-custom;} </style> </head> <body></body> </html>
后果如下:
能够看到,此状况下字体的加载是对 onload 有影响的,而后咱们又测试了一下只申明字体、不应用的状况,也就是删除下面代码中 body 设置的字体,发现这种状况下,字体是不会发出请求的,仅仅是造成了代码的冗余。
四、总结
后面列举了大量的案例,接下来咱们做个总结,实质性影响 onload 其实就是几个方面。
- 图片资源的影响毋庸置疑,无论是在页面中间接加载,还是通过 js 懒加载,只有加载过程是在 onload 之前,都会导致页面 onload 时长减少。
- 多媒体资源的期待时长会被记入 onload,然而理论加载过程不会。
- 字体资源的加载会影响 onload。
- 网络接口申请,不会影响 onload,但须要留神的是接口返回后,如果此时页面还未 onload,又进行了图片或者 dom 操作,是会导致 onload 延后的。
- 款式不会影响脚本的加载和解析,只会阻塞脚本的执行。
- 异步脚本申请不会影响页面解析,然而脚本的执行同样影响 onload。
五、优化动作
- 图片或其余资源的预加载能够通过 preload 或 prefetch 申请,这两种形式都不会影响 onload 时长。
- 肯定留神压缩图片,页面中图片的加载速度可能对整体时长有决定性影响。
- 尽量不要做串行申请,没有依赖关系的状况下,举荐并行。
- 中文字体包十分大,能够应用字蛛压缩、或用图片代替。
- 动态资源上 cdn 很重要,压缩也很重要。
- 删除你认为可有可无的代码,没准哪一行代码就会影响加载速度,并且可能很难排查。
- 视频资源如果在首屏以外,不要开启预加载,正当应用视频的 preload 属性。
- async 和 defer 记得用,很好用。
- 非必要的内容,能够在 onload 之后执行,是时候从新拾起来这个 api 了。