作者:京东科技 孙凯

一、前言

相信很多前端开发者在做项目时同时也都做过页面性能优化,这不单是前端的必备职业技能,也是考验一个前端基础是否扎实的考点,而性能指标也通常是每一个开发者的绩效之一。尤其马上接近年关,页面白屏时间是否过长、首屏加载速度是否达标、动画是否能流畅运行,诸如此类关于性能更具体的指标和感受,很可能也是决定着年底你能拿多少年终奖回家过年的晴雨表

关于性能优化,我们一般从以下四个方面考虑:

  1. 开发时性能优化

  2. 编译时性能优化

  3. 加载时性能优化

  4. 运行时性能优化

而本文将从第三个方面展开,讲一讲哪些因素将影响到页面加载总时长,谈到总时长,那总是避免不了要谈及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,加载可能有点慢:



    这个结果非常有意思,他到底发生了什么呢?

    1. 脚本请求是0.5秒的延迟,样式请求是2秒

    2. 脚本资源是 async 的请求,异步发出,应该什么时候加载完什么时候执行

    3. 但是图中的结果却是等待样式资源加载完毕后才执行

    答案就在那个仅有一个空格的脚本标签中,经反复测试,如果把标签换成注释,也会出现一样的现象,如果是一个完全空的标签,或者根本没有这个脚本标签,那下方的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 其实就是几个方面。

  1. 图片资源的影响毋庸置疑,无论是在页面中直接加载,还是通过 js 懒加载,只要加载过程是在 onload 之前,都会导致页面 onload 时长增加。

  2. 多媒体资源的等待时长会被记入 onload,但是实际加载过程不会。

  3. 字体资源的加载会影响 onload。

  4. 网络接口请求,不会影响 onload,但需要注意的是接口返回后,如果此时页面还未 onload,又进行了图片或者dom操作,是会导致 onload 延后的。

  5. 样式不会影响脚本的加载和解析,只会阻塞脚本的执行。

  6. 异步脚本请求不会影响页面解析,但是脚本的执行同样影响 onload。

五、优化举措

  1. 图片或其他资源的预加载可以通过 preload 或 prefetch 请求,这两种方式都不会影响 onload 时长。

  2. 一定注意压缩图片,页面中图片的加载速度可能对整体时长有决定性影响。

  3. 尽量不要做串行请求,没有依赖关系的情况下,推荐并行。

  4. 中文字体包非常大,可以使用字蛛压缩、或用图片代替。

  5. 静态资源上 cdn 很重要,压缩也很重要。

  6. 删除你认为可有可无的代码,没准哪一行代码就会影响加载速度,并且可能很难排查。

  7. 视频资源如果在首屏以外,不要开启预加载,合理使用视频的 preload 属性。

  8. async 和 defer 记得用,很好用。

  9. 非必要的内容,可以在 onload 之后执行,是时候重新拾起来这个 api 了。

前端里那些你不知道的事儿之 【window.onload】的更多相关文章

  1. 被引用的外部JS存在window.onload时,判断当前页面是否已存在window.onload,并进行相应处理

    如果页面a.html引用了b.js,b.js里的方法需要在页面资源加载完成后执行,即在window.onload里执行:这时如果a.html里使用了window.onload方法,b.js就不能重复调 ...

  2. 巨蟒python全栈开发数据库前端6:事件onclick的两种绑定方式&&onblur和onfocus事件&&window.onload解释&&小米商城讲解

    1.回顾上节内容(JavaScript) 一.JavaScript概述 1.ECMAScript和JavaScript的关系 2.ECMAScript的历史 3.JavaScript是一门前后端都可以 ...

  3. body里面的onload和window.onload的区别

    区别:body里面的onload是在“页面加载完成后执行的动作” window里面的onload是在“页面加载时执行的动作” 例子:在html页面中有一个大图片,想要在图片显示出来后提示一个消息框“图 ...

  4. body里面的onload和window.onload,window.load的区别

    区别:body里面的onload是在“页面加载完成后执行的动作”window里面的onload是在“页面加载时执行的动作” window.load这个应该只是表明事件方法,但并未执行,比如click表 ...

  5. 前端开发学习之——dom ready和window onload的区别

    1.ready事件是在页面中所有DOM结构已完全加载时执行,监听的是 DomContentload 事件,初始化并解析完成时触发,不需要等待样式表.图片和 iframes 加载完,也就是说当这个事件触 ...

  6. window.onload和window.document.readystate的探究

    在编写前端页面的时候,我们时常需要对页面加载的状态进行判断,以便进行相应的操作. 比如在移动端,时常需要在页面完全加载完成之前,先显示一个loading的图标,等待页面完成加载完成后,才显示出真正要展 ...

  7. $(document).ready和window.onload 简单分析区别

    <锋利的JQuery>翻开没看几页,就遇到了这个神奇的问题,之前在橙旭园学习的时候,JQuery只教了一些基础的,像链式操作等,那时以为这个和window.onload()差不多一模一样, ...

  8. window.onload与$(document).ready()的区别

    对于很多初学者来说,window.onload出现在代码中的频率非常高,这似乎变成了一种习惯,可是并不知道具体为什么要加这句代码,可以做几个试验对比: 实验一: <script> docu ...

  9. window.onload和$(document).ready()的区别

    window.onload和$(document).ready()的区别,如下表所示   window.onload $(document).ready() 执行时间 在页面所有内容(图片.文件)加载 ...

  10. JS的window.onload与JQuery的$(document).ready(function(){})的区别

    前段时间去面试被问及JS的加载(onload)与jQuery中加载(ready)方法的区别,瞬时懵逼了,关于这个知识点平时还真没怎么注意. 最近先来无事便查了一下资料, onload 事件(W3c上给 ...

随机推荐

  1. 第15章 授权:保护您的应用程序(ASP.NET Core in Action, 2nd Edition)

    本章包括 使用授权控制谁可以使用你的应用 对策略使用基于声明的授权 创建自定义策略以处理复杂的需求 根据所访问的资源授权请求 隐藏用户未经授权访问的Razor模板中的元素 在第14章中,我向您展示了如 ...

  2. 12.21linux学习第二十天

    今天老刘讲最后一章,第20章使用LNMP架构部署动态网站环境.定制安装17个安装包,编译安装要花很长时间,需要漫长的等待. LNMP动态网站部署架构是一套由Linux + Nginx + MySQL ...

  3. 中值滤波 ordfilt2函数

    滤波是过滤掉信号中没用的波段 ordfilt2函数语法格式为:B=ordfilt2(A,order,domain)B=ordfilt2(A,order,domain,S)B=ordfilt2(..., ...

  4. Qt滚动条样式

    Qt手动设置滚动条的qss样式 QScrollBar:vertical { width: 14px; margin: 16px 0 16px 0; background: #eaeaea; } QSc ...

  5. Javaheima21

    Java 学习内容 XML XML解析技术 XPath 设计模式 在有些业务场景下,存储数据或者传输数据给别人的时候,数据需要满足优点的规范进行组织 XML文件存储的数据需要提取出来 如何方便的在XM ...

  6. C#获取本地所有打印机名称

    引用:using System.Drawing.Printing; 实现代码: List<string> PrintS = new List<string>(); foreac ...

  7. 九九乘法表打印记一次al面试

    for (int i = 1; i <= 9; i++) { for (int j = 1; j <= i; j++) { System.out.print(i + "x&quo ...

  8. 拖拽改变div宽、高(转)

    $(function () { //绑定需要拖拽改变大小的元素对象 bindResize(document.getElementById('test')); }); function bindResi ...

  9. 初次使用Sqoop报错,sqoop命令不能正常使用:hcatalog does not exist!accumulo does not exist!

    1.问题描述: (1)问题示例: [hadoop@master Tmp]$ sqoop  helpWarning: /home/grid/Sqoop/sqoop-1.4.7/../hcatalog d ...

  10. C#中字符数组,字节数组和string之间的转化(转)

    原文链接:http://hi.baidu.com/endyli/item/7bf074945de35e1f934f41fe 来源: NDC(NetworkDiskClient)的界面和后台程序之间用S ...