热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

不简单的前端性能优化

本文主要介绍“关键渲染路径”与“网络”两个方面的性能优化并提供demo,篇幅较长建议电脑观看。前端优化的方面太多,本文介绍的仅仅是其中的一部分ÿ

本文主要介绍“关键渲染路径”与“网络”两个方面的性能优化并提供demo,篇幅较长建议电脑观看。

前端优化的方面太多,本文介绍的仅仅是其中的一部分,力求涵盖“关键渲染路径”的方方面面,及一些不常被提到的“网络优化”部分。

测试环境如无特殊说明均为Chrome 57


渲染页面过程

浏览器从打开一个URL到渲染完页面共有:

  • 下载HTML文档

  • 下载HTML文档中的css

  • 下载Js文件

  • 执行js脚本

  • 下载其他资源

  • 通过HTML文档构建DOM(Parse HTML)

  • 通过CSS文件构建CSSOM(Parse CSS)

  • 通过DOM与CSSOM计算render tree

  • 根据render tree进行绘制,计算各个元素位置与大小(Layout)

  • 对页面进行上色,渲染为最终显示的像素(Paint)

第一次完成Paint称为“初次渲染”,这时候用户就能看到render tree里面的东西了。而完成初次渲染的过程称为“关键渲染路径”,关键渲染路径上需要加载的资源叫做“关键资源”

这个过程很多很复杂,其中的依赖关系也很复杂,笔者尝试画图来表示,但是实在是没画出来,所以还是用文字来表述吧:

  • 引入的资源,哪怕被阻塞(比如被js脚本阻塞后续link标签),浏览器依旧会智能的预先加载它们(但是不执行)

  • “CSS文件的加载”会阻塞“Js文件执行”。若CSS引用在Js文件之前,“加载CSS文件”会阻塞“Js文件执行”。即CSS文件未加载解析完成前,js文件不会得到执行。因为js有可能会修改CSSOM。带有async和defer属性的script不受限制。

  • Parse HTML的解析是增量的,因此浏览器可以边下载HTML边构建DOM树

  • “CSS文件的加载”会阻塞“Layout”。若页面有正在加载的CSS文件,在CSS文件加载完之前,浏览器不会对页面进行Layout,这是为了防止样式突变带来的抖动

  • “加载Js文件”会阻塞“Parse HTML”,这个估计大家都知道了,因为js可以通过document.write修改HTML文档流

  • “Js文件执行”会几乎会阻塞所有东西,包括Layout

比较有意思的是,字体的加载会阻塞局部的渲染。若某一段文本的字体使用了一个尚未加载完的字体,这段文本则先不会被Paint,直到字体加载完或者超过某个时间(通常是3秒)文本才会突然显示。

浏览器为了避免FOUT(Flash Of Unstyled Text),会尽量等待字体加载完成后,再显示应用了该字体的内容。只有当字体超过一段时间仍未加载成功时,浏览器才会降级使用系统字体。每个浏览器都规定了自己的超时时间(Chrome是3秒)。但这也带来了FOIT(Flash Of Invisible Text)问题。内容无法尽快地被展示,导致空白

一些Demo来解释浏览器渲染流程

CSS会阻塞Layout:Demo





Hello

World



CSS会阻塞Js执行:Demo





Hello

World



Js执行会阻塞关键渲染路径,哪怕是defer还是async:Demo





Hello

World



Foot会阻塞局部渲染,但是智能的浏览器会给他设定一个上限,一般是3秒钟:Demo





Hello

World



CSS篇优化策略

优化核心概念是:将初次渲染不需要的CSS想办法剥离出关键渲染路径

如果仅仅是为了提前初次渲染时间而进行优化,将页面必备的CSS剥离关键渲染路径而造成样式突变导致页面抖动,则得不偿失了

使用link/style的media属性

对某些媒体查询条件触发后才使用的css,可以在link标签中加入media属性,如下:

此样式表仍会加载。当浏览器环境不匹配媒体查询条件时,该样式表不会阻塞渲染。我们可针对不同媒体环境拆分CSS文件,并为link标签添加媒体查询,避免为了加载非关键CSS资源,而阻塞初次渲染

使用DOM API添加CSS

可以使用js代码来添加css

var style = document.createElement('link');
style.rel = 'stylesheet';
style.href = 'index.css';
document.head.appendChild(style);

使用resoure hint规范的preload

将link标签的rel属性设置为preload,浏览器遇到遇到标记为preload的link时,会开始加载它,但是由于rel不是stylesheet,因此不会阻塞渲染。

然后在适当的时候,在rel改为stylesheet,即可应用此样式。

但是这个属性兼容性比较差,详细可以参考这里。不过有一个polyfill可以用loadCSS,原理是通过DOM API插入样式资源。

这个属性的使用情景有些偏,也可能是我理解问题:

当使用preload引入css文件时,实际上证明这个页面根本不需要这个css,它有可能是打印样式,或者是响应式网站的另一套css代码。但是,使用preload属性,浏览器反而会预先加载它,也就是说,在window.onload之前,用户将耗费了网络资源在加载一个暂时不需要的样式。网络资源不可能是无限的,也就是说这个css会占用页面其他资源比如图片的网络资源。

询问瓜瓜老师本人后,瓜瓜老师说:

举个例子。第三屏有个广告版,它的样式

这样确实这个css的紧急程度就介于关键渲染路径的css与页面图片之间了,不过貌似这个情景很受限。

JS篇优化策略

使用defer延迟脚本执行

当script标签拥有defer属性时,该脚本会被推迟到整个HTML文档解析完后,再开始执行。因此将脚本放在head中,可以提早浏览器对脚本文件的加载,但是却不会阻塞parse HTML。



注意,defer的脚本不会被css阻塞,parse HTML完成后立即执行,但是有可能会阻塞关键渲染路径。为什么说有可能呢,假如脚本文件在render tree生成前加载完毕,则会开始执行,执行过程中会阻塞关键渲染路径。请参考这个Demo

被defer的脚本,在执行时会严格按照在HTML文档中出现的顺序执行,但是实际上貌似不是这样,js文件前后文件若有依赖需慎重使用。

使用async延迟脚本执行

和defer类似,只是当js加载完后马上执行,而不在乎parse HTML是否完成,因此假如脚本比css先加载完,也会阻塞关键渲染路径。



使用DOM API

据笔者所知,这是唯一一种100%不会阻塞关键渲染路径的js脚本加载方式。通过DOM API引入的js脚本会等到页面Layout和Paint后再开始执行,不论你将载入js文件的代码放在head中还是body后面亦是如此。

其他的优化

使用Web Font Loader加载字体

若不想让字体阻塞局部渲染,可使用Web Font Loader

网络优化篇

网络优化和CSS优化策略相同,尽可能让关键资源提前加载完,所以优化时尽量将以下指标压缩到最低:

  • 关键资源数

  • 关键资源体积

  • 关键资源网络来回数

当然,如果你的项目使用了先进的SPDY或HTTP/2,下面的方法可能并不适用。

优化关键资源数

RFC2616规定同域名同时只能有 2 个连接(RFC7230 中无限制),而​​​​​​​现代浏览器一般允许同域6个并发连接。因此,当页面中有许多需要外链的资源(script、link等),浏览器最多在每个域同时并发下载6个。

每一个请求,若使用域名,则需要额外增加一次DNS查询时间(若缓存未过期会命中缓存),因此一个网站过多的使用不同域名的资源会额外增加DNS查询开销,这点在移动端非常明显。

当然,每个请求建立根据TCP协议规定,还需要先进行3次捂手才可以建立链接。

合并请求

尽可能的合并请求,减少网络请求数。这一点可能在其他性能优化文章都说烂了:

  • 小图片转base64

  • 合并打包CSS、JS文件

现在的比较流行的webpack就非常擅长做这种事情

适度使用内联CSS和Js

使用内联的CSS和JS固然可以减少请求,但是使用内联也意味着你的CSS和JS将不会再被浏览器缓存,因此要适度的使用内联,内联不是万能的。

从HTTP协议下手

最佳方案肯定是过渡到HTTP/2无疑,但是现在HTTP/2的支持并不算太好,而且各大浏览器仅支持TLS下实现的HTTP/2(说白了就是HTTPS),使得HTTP/2的使用存在许些限制。

如果没有HTTP/2,或许可以:

  • 使用Keep-Alive可以规避TCP三次握手的时间

  • 使用Transfer-Encoding:chunked分块输出文件,还记得parse HTML的过程是增量的吗?若浏览器可以边下载HTML文件边解析,岂不美哉?

  • 减少重定向,这个看上去理所当然但是实际上却很容易被忽略

适度使用域名散列

浏览器同域并行下载数量有限,所以只要多建立几个二级域名就好了,然后合理的分配各个资源就好了。

假如由于某些不可抗拒原因,关键资源数是12个,那么只要建立2个二级域名分别分配给其中的12个资源,浏览器会同时并行下载它们了。

不过,使用域名散列要适度,每一个域名都需要额外的增加一次DNS查询时间。当然,DNS本身也有缓存,或许适当的增加DNS TTL时间也是个不错的主意。

压缩关键资源体积

对于js、css文件,现在网上现成的压缩工具一堆,而且应用十分广泛,相信大家都知道了,这里就不多说了。

说到压缩,服务器开启一定的压缩策略(如gzip)是个不错的主意,效果拔群,资源大概会压缩到原有的1/3左右。

图片压缩,这个需要知道什么情境下适合什么类型的图片,GIF、JPG、PNG使用情景各不相同,具体可以参考这篇文章:图片格式那么多,哪种更适合你?

关键资源网络来回数

假如一个页面需要引入2个CSS才能工作,下面有2种方式

  • 2个均用link引入

  • 1个用link引入,在css中import另一个css

毫无疑问肯定是前者快,因为前者的网络来回数是1,而后者是2。

因此,尽可能将资源加载扁平化,减少关键资源网络来回数是个不错的主意。

当然,优化时要注意的点也有不少,比如前面提到的浏览器同域并发限制等,需要权衡使其不要影响到其他的导致初次渲染时间延后。

一些无效的优化策略

使用document.write打印link标签引入css仍会阻塞初次渲染。

引用

奇舞团@瓜瓜老师:

  • PPT:https://ppt.baomitu.com/d/258...

  • 个人主页:http://melonh.com/

奇舞团@屈屈老师:

  • PPT:https://ppt.baomitu.com/d/a8a...

  • 个人主页:https://imququ.com/

W3C规范:

  • https://www.w3.org/TR/2016/RE...




推荐阅读
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • 高质量SQL书写的30条建议
    本文提供了30条关于优化SQL的建议,包括避免使用select *,使用具体字段,以及使用limit 1等。这些建议是基于实际开发经验总结出来的,旨在帮助读者优化SQL查询。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 本文介绍了指针的概念以及在函数调用时使用指针作为参数的情况。指针存放的是变量的地址,通过指针可以修改指针所指的变量的值。然而,如果想要修改指针的指向,就需要使用指针的引用。文章还通过一个简单的示例代码解释了指针的引用的使用方法,并思考了在修改指针的指向后,取指针的输出结果。 ... [详细]
  • Html5-Canvas实现简易的抽奖转盘效果
    本文介绍了如何使用Html5和Canvas标签来实现简易的抽奖转盘效果,同时使用了jQueryRotate.js旋转插件。文章中给出了主要的html和css代码,并展示了实现的基本效果。 ... [详细]
  • CentOS 7部署KVM虚拟化环境之一架构介绍
    本文介绍了CentOS 7部署KVM虚拟化环境的架构,详细解释了虚拟化技术的概念和原理,包括全虚拟化和半虚拟化。同时介绍了虚拟机的概念和虚拟化软件的作用。 ... [详细]
author-avatar
安静的美男只
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有