前端里那些你不知道的事儿之 【window.onload】

作者:京东科技 孙凯

一、前言

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

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

  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 了。

0 条评论
请不要发布违法违规有害信息,如发现请及时举报或反馈
还没有人评论呢,速度抢占沙发!
相关文章
  • 前言本文属于《前端工程化系列》的标准&规范介绍,该系列主要介绍我们一整套前端工程化解决方案。文章会由浅入深的来介绍如何实现前端工程化,以及一整套开箱即用的开源前端工程化解决方案。文章主要面向中小型团队...

  • Vins 前端中高效的去畸变的方式解析 1.0 畸变是如何产生的 \(\quad\)我们先来想想3D点是如何投影到图像平面的:世界坐标点经过一个外参矩阵得到相机坐标系下的位置,由于我们经常用到的是归一...

  • 这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 这个问题? 这个问题一般会出现在面试题里面,然后回答一些诸如轮询、WebSocket之类的答案。当然,实际开发中,也会遇到类似别人给你...

  • 第一篇章:吸引HR 如果你想在众多简历中脱颖而出,需要注意以下几点: 1、突出你的亮点: 给你的简历一个吸引人的文件命名和头部,突出你的关键技能和经验。 2、采用简洁的语言: 用简单易懂的语言来...

  • 无序列表 无序列表的标签: 无序列表列表项的标签: ul标签中只能嵌套li标签,不能存放别的标签或者数字,li标签之中可以存放任何元素和标签 无序列表会默认在每个列表项前面增加一个小点,如下图所示: ...

  • 1.JS自定义烟花特效 这是一款基于JS和Canvas的自定义烟花特效,初始化界面的时候特效是不带声效的绽放,当你点击顶部中间的播放,即可以看到美丽的烟火也可以听到烟花绽放的声音,让你脑海浮现过年团圆...

  • 文章都在个人博客网站:https://www.ikeguang.com/ 同步,欢迎访问。 最近看到有人在用flink sql的页面管理平台,大致看了下,尝试安装使用,比原生的flink sql界面确...

  • 记录ArcGIS处理三维bim模型全纪录,从原始的rvt格式开始,到最后web前端js api调用的整个过程,并记录部分中间操作过程中出现的问题和解决办法。 本文示例使用: 软件:ArcGIS Pro...

  • 当用户在浏览器的地址栏中输入 URL 并点击回车后,页面是如何呈现的。 简单来说,当用户在浏览器的地址栏中输入 URL 并点击回车后,浏览器从服务端获取资源,然后将内容显示在页面上。这个过程经过了:浏...

  • 这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 性能优化一直是前端研究的主要课题之一,因为不仅直接影响用户体验,对于商业性公司,网页性能的优劣更关乎流量变现效率的高低。例如 Doubl...

  • 购买阿里云服务器 阿里云服务器ECS 系统镜像使用Ubuntu 20.04 LTS 使用ssh连接服务器,终端或者CMD中执行:$ssh root@x.x.x(阿里云服务器账号名@公网地址) 输入账号...

  • SMQTTX介绍 SMQTTX是基于SMQTT的一次重大技术升级,基于Java开发的分布式MQTT集群,是一款高性能,高吞吐量,并且可以完成二次开发的优秀的开源MQTT broker,主要采用技术栈:...

  • 作为一个技术博主,了不起比较喜欢各种折腾,之前给大家介绍过 ChatGPT 接入微信,钉钉和知识星球(如果没看过的可以翻翻前面的文章),最近再看开源项目的时候,发现了一个 ChatGPT Web UI...

  • 第1章 什么是severless 什么是NoOps 利用自动化运维代替手工运维模式 什么是severless 开发者无需关注服务器资源配置情况、部署情况、操作系统以及依赖软件等在内等所有细节,这一切都...

  • 网页 1. 什么是网页 网站是指在因特网上根据一定的规则,使用HTML等制作的用于展示特定内容相关的网页集合。 网页是网站中的一“页”,通常是HTML格式的文件,它要通过浏览器来阅读。 网页是构成网站...

  • RxEditor是一款开源企业级可视化低代码前端,目标是可以编辑所有 HTML 基础的组件。比如支持 React、VUE、小程序等,目前仅实现了 React 版。 RxEditor运行快照: 项目地...

  • 集算表 (Table Sheet)是一个具备高性能渲染、数据绑定功能、公式计算能力的数据表格,通过全新构建的关系型数据管理器结合结构化公式,在高性能表格的基础上提供排序、筛选、样式、行列冻结、自动更新...

  • WEB开发会话技术03 10.问题引出 问题引出 不同的用户登录网站后,不管该用户浏览网站的哪个页面,都可以显示登录人的名字,还可以随时去查看自己购物车中的商品,这是如何实现的呢? 也就是说,一...

  • 网络也是前端性能优化的重要一环,网页上的资源都要经过网络来传输。   优化网络性能除了缓存和压缩之外,还有就是协议和 CDN。   HTTP 协议已经历了多个版本,每个版本的出现其实就是为了解决已知的...

  • “前端已死”的论调,每隔一段时间就会被翻出来重新讨论,除了先前人们担忧的低代码对前端开发者的影响,还有最近爆火的chatGPT、GPT-4等。作为前端开发者,我非常不认可“前端已死”论,不要贩卖焦虑,...