页面加载过程详解和优化策略

前言

打开浏览器,敲下一串地址,回车,页面便能显示出来。作为一个普通用户,大家可能只关心页面所展示的内容,是不是有趣啊?能不能够吸引到人。

但作为一个前端开发工程师,尤其一个还在试图提升自己的前端开发工程师,我们除了实现页面的基础功能,完成任务,还得进一步去思考,从回车这个动作开始,到有内容展示在页面上,这中间到底经历哪些过程?

这些思考并不是浪费时间,通过了解这中间的每一个过程,我们将能够得到有效地、有针对性地获得若干,关于页面性能优化的建议。

这便是本文所包含的内容。

概述

简单来说,从地址被敲下,到有完整内容显示出来,包含如下图所示的若干过程。

页面完整加载过程

而通过HTML5所提供的Performanc API,我们将可以获得这中间每一个过程的精确耗时。

let timing = window.performance.timing;
console.log(timing);

/*
* PerformanceTiming{
* navigationStart:1675833793962,
* redirectStart:0,
* redirectEnd:0,
* fetchStart:1675833793964,
* domainLookupStart:1675833793964,
* domainLookupEnd:1675833793964,
* connectStart:1675833793964,
* secureConnectionStart:0,
* connectEnd:1675833793964,
* requestStart:1675833793973,
* responseStart:1675833794010,
* unloadEventStart:0,
* unloadEventEnd:0,
* responseEnd:1675833794011,
* domLoading:1675833794014,
* domInteractive:1675833794248,
* domContentLoadedEventStart:1675833794248,
* domContentLoadedEventEnd:1675833794248,
* domComplete:1675833914363,
* loadEventStart:1675833914363,
* loadEventEnd:1675833914363,
}
*/

有一些字段很容易理解,通过名称就能猜出大体含义,另外一些却还模模糊糊,云里雾里。

不过没关系,接下来,通过具体分析每一个过程,我们也将清楚了解这些属性所代表的含义,以及如何使用它们来记录我们页面的性能表现,继而针对性地进行优化。

第一个过程:DNS解析

当我们在浏览器的地址栏里敲下一串地址并回车,浏览器就会开始根据这串地址,去寻找提供服务的服务器,继而对服务器发起请求。
寻找服务器的过程,我们称之为DNS解析。

下面是百科上关于它的介绍:

域名系统(英文:Domain Name System,缩写DNS)是互联网的一项内核服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网,而不用去记住能够被机器直接读取的IP数串。
DNS是具有树型结构的名字空间,核心功能是完成域名到IP地址的转换,使用TCP和UDP端口53。当前,对于每一级域名长度的限制是63个字符,域名总长度则不能超过253个字符。

具体来说,DNS解析,包含如下过程:

  1. 查找浏览器缓存
    考虑到网络请求总会产生一定消耗,在真正开始寻找前,浏览器会先尝试读取缓存。DNS缓存是分层级的,第一层就保存在浏览器自己家里,所以它第一个查找的地方,也是在自己家。
    不同浏览器缓存时间不同,如果这时候缓存还没过期,找到了,那么结果将被直接返回,DNS查询也到此为止。

  2. 查找系统缓存
    如果浏览器自己家里没缓存,或者缓存过期了,找不到了,那么接下来它就会去操作系统的缓存里找。
    除了系统的缓存,我们也可以手动配置host文件,这样浏览器会优先使用我们的配置。

  3. 查找路由器缓存
    如果系统缓存里也没有,接下来就会找到路由器,——是的,路由器里也有缓存,很神奇,是不是?

  4. 查找运营商DNS缓存
    路由器里找不到,那就只能去找运营商要。

  5. 递归搜索
    如果运营商那里也没有需要的数据,那就只能开启消耗最大的递归搜索。

    举个例子,假如我们现在想要访问m.taobao.com,浏览器又在各级缓存里都找不到它,那么接下来,它就将进行如下的一系列操作。

    1. 对域名进行一个简单的分割,把它变成独立的三个部分:顶级域名(com)、一级域名(taobao)、二级域名(m);
    2. 向根域名服务器,请求顶级域名(com)的IP地址;
    3. 获取返回值,向顶级域名(com)的服务器,请求一级域名(taobao)的IP地址;
    4. 获取返回值,向一级域名(taobao)的服务器请求二级域名(m)的IP地址;
    5. 获取返回值,完成查找,返回m.taobao.com的IP地址;

    这个过程,根据网络状况的不同,耗时可能高达几秒,甚至十几秒。

DNS解析过程的精确耗时,可以通过如下方式获得。

let dns = timing.domainLookupEnd - timing.domainLookupStart;

接下来是优化建议。

  1. 因为域名解析是有消耗的,并且可能消耗还不小,那么为了提高资源的加载速度,我们首先可以尝试减少页面中使用到的域名数量。
  2. 另外一个方法是开启DNS预解析,将需要访问的资源链接,写在HTML头部,告诉浏览器去提前解析它们的域名。需要明确的一点,在http协议下,浏览器会自动预解析a标签、image标签所携带的资源链接,https协议下则需要我们去手动开启。
<!--告诉浏览器提前解析https://example.com/-->
<link rel="dns-prefetch" href="https://example.com/">
<!--手动开启预解析-->
<meta http-equiv="x-dns-prefetch-control" content="on">
  1. 试试HTTPDNS,减少时延的同时,还能防运营商劫持。

建立连接

你可以把这个过程想象成架设通道,你想架设一条从你家,到张三家的通道,但因为张三跟你不熟,他甚至不认识你,所以在架设之前,你需要先征求他的同意,你们俩协商的过程,就是建立连接的过程。

TCP三次握手

耗时计算:

let tcp = timing.connectEnd - timing.connectStart;

优化点如下:

  1. 避免重定向。
  2. 适当的合并请求。
  3. 长链接keep-alive。

发送请求

连接建立成功后,请求数据就可以发送了,这个过程从请求被发送开始,到得到响应结束。它的耗时计算如下:

let req = timing.responseStart - timing.requestStart;

优化点如下:

  1. 避免重定向。
  2. 减少请求数据量,这里重点检查是否存在冗余的cookie。
  3. CDN服务可以帮助缩短传输链路,继而提高传输速度。

接收数据

把本地和服务器之间所建立的连接想象成管道,从中传输的数据就是管道中的水流,水流从一端流向另一端,流完了,数据传输也完毕了,这个过程的耗时计算如下:

let res = timing.responseEnd - timing.responseStart;

想要降低这个过程的耗时,一个办法是降低传输的数据量,另外一个就是缩短传输链路,具体如下:

  1. 减小html代码体积,包括但不限于:删除冗余代码、进行代码压缩。
  2. 在传输过程开启Gzip,进一步压缩传输的数据量。
  3. 使用CDN服务,来缩短传输链路。

构建DOM树

拿到HTML代码,浏览器首先需要对其进行解析,生成DOM树,DOM树详细生成过程,可以去看作者的另外一篇文章。这个过程的耗时,通过如下公式计算:

let dom = timing.domInteractive - timing.domLoading;

domInteractive的字面含义看起来是DOM可交互,但事实上,事情进展到这个阶段,还远远没到可交互的脚步。
它只是表示,我们的DOM树已经构建好了,可以用来渲染了,但在渲染之前,它还需要等待一切阻塞渲染的工作全部完成,比如CSSOM的构建、比如脚本的执行,除此之外,可能还有图片、字体的加载。

想要缩短这个过程的耗时,可以从下面几点入手:

  1. 简化DOM结构,避免深层嵌套;
  2. 同步的JS会阻塞该解析过程,可以尝试对JS进行分段加载和延迟加载;

网页加载完成

拥有defer属性的脚本下载并执行完成后,DOMContentLoaded事件便会被触发,这个过程的耗时,就是拥有defer属性的JS脚本的执行时间。它的耗时,通过如下公式计算:

let dom = timing. domContentLoadedEventStart - timing.domInteractive;

通过分析构建DOM树的时间,我们可以针对性地优化HTML,而通过分析异常的网页加载完成时间,我们可以发现JS代码执行中的问题。
有一本书《高性能JavaScript》,专门讲了JS的优化问题,这里就只给出几个点:

  1. 减少需要加载的文件数,尽量合并代码。
  2. 减小变量调用链路,多次访问到的对象成员保存成局部变量。
  3. 缓存函数运行结果。
  4. 算法优化,减少迭代次数,以及迭代的工作量。
  5. 缩短函数调用链。

DOM加载完成

defer脚本下载并执行完成后,浏览器还会等待CSS样式的解析。
等CSSOM准备好了,它便会将它跟之前构建好的DOM树合并在一起,构建成一棵完整的Render树。接下来就可以开始绘制页面了。
等页面绘制完成,其中的图片、字体等资源也都加载完毕,整个加载过程,才算真正的完成。load事件也将随即便触发。这个过程的耗时,可以这么计算:

let domComplete = timing.domComplete - timing.domContentLoadedEventEnd;

根据描述,可以知道,这一处的耗时,主要花费在资源的加载上,所以优化也集中在资源的加载和展示方向。

  1. 减少资源的加载,尽量使用css、iconfont和svg,替代图片。
  2. 根据屏幕分辨率进行适配。小屏幕不需要使用太清晰的图片,那属于浪费资源。
  3. 使用合适的图片格式,兼容的情况下,webp是个不错的选择。
  4. 小图片可以使用data url代替。
  5. 不需要在首屏使用的资源,请延迟加载。

此外,DOMContentLoaded事件耗时DOMLoad事件耗时,主要就是执行JS代码的耗时,优化建议可以参考网页加载完成

以上~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容