- 地址:
- 海南省海口市
- 邮箱:
- admin@youweb.com
- 电话:
- 0898-08980898
- 传真:
- 1234-0000-5678
大家知道。我们每天都在谈前端性能优化,每天都在背前端性能优化方案,然而,我们却不知道他背后的原理以及涉及那些知识储备。所以,我问了自己一些问题,
- 1、我们为什么要做前端性能优化?
- 2、性能优化的标准是什么?
- 3、我们每天都在谈性能优化到底在谈什么?
- 4、我们应该从那些角度去谈性能优化?
- 5、他背后优化的原理是什么?
- 6、做性能优化的时候需要考虑什么?
当我问了自己一些问题以后,我重新学习性能优化的脉络就很清晰了,接下来我们来我们逐一解决。
在前端的职业生涯中,我们总是能一次次听到“性能”和“体验”这两个词。而在慢慢从菜鸟一点点打怪升级的过程中,这两个词听到的频率在逐步上升。
而很多人只知道,这个东西,面试要考,于是开始背!背!背! 却从未想过,我们做性能优化的初衷是什么。项目在什么阶段应该怎去做什么样的优化。怎样去权衡可维护性和优化的平衡点。这其实都是我们去做性能优化需要思考的问题,那么有了大量的思考,其实我们心中的答案也就能呼之欲出。这也是我要叮嘱自己的:做一些事情之前,要去思考他背后本质是什么,而不是流于表面,人云亦云。
那么我们为什么要去做性能优化呢?什么样的项目必须要去做性能优化?什么样的项目可以牺牲一些优化的点,而换取项目稳定性,和可维护性!
我们知道,一个网站最重要的的就是用户,有了用户你才能有业务,打个比方,你是一个电商网站,那么你一定希望你的用户非常多,只有这样你才能有更多的人去浏览你的商品,从而在你的网站上花钱,买东西,这样你才能产生收益,在比如,为了获取更多的用户,你必须要在借助第三方的工具去推广你的网站,比如搜索引擎。而现在的搜索引擎,他都会去对你的网站做性能评估,从而可能影响你的排名!
如此一来,你就会知道,我们所谓的性能优化其实就是留住用户,以及获取用户,那么,基于以上思路,你就能根据当前项目,判断出我当前网站应该给予什么样的特殊优化方式,以及那些通用优化方式,而不是在网上找一通,然后对着优化!
知道了上述问题之后,我们则需要找寻一些标准,以及性能瓶颈。来达到优化的目的
记得亚马逊做个一个调查,他发现一个网站每100ms的延迟则导致1%的销量损失,那么,我们的网站延迟要到什么地步才算性能好呢? 所以我们要有一个标准!
上述说过,每个项目的情况不一样,我们不能跟亚马逊一样,做到极致,所以,大多数网站这需要遵循一个标准,我们认为达到这个性能指标,就算可以了,在某个高频操作的点,在做针对的优化!
测量页面的加载性能是一项艰难的任务。因此 Google Developers 正和社区一起致力于建立渐进式网页指标(Progressive Web Metrics,简称 PWM’s)。
PWM’s 都是些什么,我们为什么需要它们?
这就牵扯到浏览器的历史了。为了能完全的讲明白这一块,我们从头讲起,在很久很久以前,我们有两个主要的点(事件)来测量性能:
DOMContentLoaded?—?页面加载完成但脚本文件刚刚开始执行时触发这里指初始的 HTML 文档加载并解析完成,但不包括样式表、图像和子框架的加载完成,参考 MDN DOMContentLoaded 事件
load 事件在页面完全加载后触发,此时用户已经可以使用页面或应用。
拿掘金举例,最下部的地方就可以看见DOMContentLoaded、load
而时至今日在交互复杂,页面内容复杂的今天,你会发现DOMContentLoaded、load并不像以前那样能真实反映出用户的体验了。他跟你页面的复杂度、交互难度、动画多少、等都有很多联系,拿bilibili和掘金举例
你会发现,bilibili远比掘金的加载时间长的多。但是哔哩哔哩的性能却不比掘金差
在现在的web页面中,DOMContentLoaded 的问题在于不包含解析和执行 JavaScript 的时间,如果脚本文件太大,那么这个时间就会非常长。比如移动设备,在 3G 网络的限制下测量跟踪时间轴,就会发现要花费差不多十秒才能到达 load 点。 另一方面,load 事件太晚触发,就无法分析出页面的性能瓶颈。 所以我们能否依赖这些指标?它们到底给我们提供了什么信息? 而且最主要的问题是,从页面开始加载直至加载完成,用户对这个过程的感知如何?
你刷新哔哩哔哩页面后,你会发现哔哩哔哩的体验非常好,除了有着好的设计之外,他还有着很多针对的性能优化。比如,快速加载首屏,其他屏幕懒加载,善于利用缓存等。
说了这么多什么叫做 PWM’s呢?
PWM’s 是一组用来帮助检测性能瓶颈的指标。除开 load 和 DOMContentLoaded,PWM's 给开发者提供了页面加载过程中更多更详细的信息
其实在谷歌浏览器中,我们就能使用devtools 来查看各种指标的加载时间!我们还是拿哔哩哔哩举例
首先打开performance 点击刷新按钮,
如此一来我们就能看到一些关键的渲染节点
这是谷歌的开发者工具还给我们提供了一个指标 - FP。这个指标表示页面绘制的时间点,换句话说它表示当用户第一次看到白屏的时间点,(fp还有另外一个意思就是函数式),FP 事件在 Graphic Layer 进行绘制的时候触发,而不是文本、图片或 Canvas 绘制的时候,所以,这个时间点用来测量性能,着实有点棘手,于是,谷歌还给我们提供了另一个。
这是当用户看见一些“内容”元素被绘制在页面上的时间点。和白屏是不一样的,它可以是文本的首次绘制,或者 SVG 的首次出现,或者 Canvas 的首次绘制等等。
FCP 事件在文本(正在等待字体文件加载的文本不计算在内)、图片、Canvas 等元素绘制时被触发。结果表明,FP 和 FCP 的时间差异可能从几毫秒到几秒不等。这个差别甚至可以从上面的图片中看出来。
所以你的内容绘制时间过长,这说明你的资源文件可能过大,或者网络拉胯。他能真是的反应网页性能方面的一些问题
LCP一种新的性能度量标准,LCP 是一种侧重于用户体验的性能度量标准,与现有度量标准相比,更容易理解与推理。 他与被舍弃的FMP不同的是,FMP是有意义的内容绘制时间点,那么这个有意义的判定就放在了谷歌开发者工具这块,他是有争议的,而且并不能说明性能问题。
W3C Web性能工作组的讨论和Google的研究,发现度量页面主要内容的可见时间有一种更精准且简单的方法是查看 “绘制面积” 最大的元素何时开始渲染。这个最大的篇幅的内容渲染他极有可能是主要内容。所以,顺理成章的代替了fmp
接下来我们再来看这一张图
上面的一张张图片表示在各个时间点,当前网页渲染的快照,而蓝色的折线,则是当前网页的内存占用,从当前折线你可以清晰的看出垃圾回收(gc)在什么时候开始的,如果你看当前的蓝色折线是一直无限上升的,那么极有可能是发生了内存泄露了,他能很好的帮你定位问题。
我们点开这里面的main 选项,就能清晰的知道,当前页面中所有长任务,以及渲染任务的耗时,和执行顺序(所谓长任务:长任务就是指解析、编译或执行 JavaScript 代码块)我们知道js和渲染是互斥的,所以在图中也可以清晰的体现到,他们互斥的关系以及谁妥协谁。这个图他还有一个洋气的名字叫--火焰图
那么通过他,仔细看
就能看到有个红色的三角,这就是谷歌工具给我们的提示,表示性能不达标了!通过些就能分析出一些关键的性能瓶颈和可优化的点。
有很多小伙伴会就会说了,这又不是我的网站,我看他干啥,我只想看看这个网站到底性能如何!谷歌也给我们提供了一个工具--lighthouse 之前需要下载,现在被开发者工具集成了
同样拿哔哩哔哩来举例,在开发者工具中测量一下,之后就会返回一堆数据,我们只关注performance部分,首先他会有一个总体得分,然后就是各个项的时间,接下来逐个分析一下
- first contentful paint --渲染出首个文本或者首张图片的时间
- Time to Interactive -- 可交互时间也就是能实现交互功能的时间
- Speed Index -- 表示网页内容的可见填充速度
- Total Blocking Time -- 从看见内容到可以交互的时间
在这里我们只关注这四点 尤其需要关注第一点和第三点。 哔哩哔哩中我们看他的first contentful paint性能还可以,如果是红色的那么就是超标了。 Speed Index 其实就是速度指数,在谷歌给的标准中,速度指数的标准是4秒 而哔哩哔哩的4.3秒也还凑活。
再往下看就是一些需要优化的项,包括http、js、css层面的优化。
在之前的first contentful paint 中,我们发现他的时间消耗主要有两部分组成,第一个是渲染时间,另一个就是网络加载时间。
我们打开network 查看一下网页资源的加载图,同样的这个图也有一个很专业的名字,叫做瀑布图
他非常直观的描述了网站资源的加载时间和顺序,这个图呢有两个解读方法,一个是横向看,另一个是纵向看。 横向看我们能看到具体加载的资源
我们看其实下载资源是最后一个步骤,他还包括等待时间,他需要排队,耽误了5毫秒,可能由于达到了浏览器最大请求数量阻塞了200毫秒。接下就是发送用了0.18毫秒。TTFB时间--是后台的处理时间以及网络传输时间,我们才能下载
纵向看就能看到资源的加载顺序,那么我就有可能让某些时间长的请求,提前加载,并行请求,来达到优化的目标。
在理解了上面这些概念之后,我们再来了解一下Chrome 团队提出了 RAIL 模型。
- response -- 处理时间应在50ms以内完成
- 动画 -- 每10ms产生一帧
- idle -- 尽可能增加空闲时间
- load -- 希望在5s之内完成内容加载并可以交互
当然这只是一个参考,在复杂的项目中,我们只能尽量的追赶,却很难达到,因为除了项目比较复杂之外,还有很多事情不是我们能做主的。比如,网站支不支持http2,你也说的不算。如果idle 我们需要达标,理论上说,洗数据的js计算,后端能做的绝不让前端去做,然而现实是,在我的职业生涯中,基本每天都在洗数据。
性能优化我们每天都在谈,突然有一天我我回过头来思考,我们每天谈的性能优化,真的是在谈这些优化的点吗?不是不是。我们说之所以能做性能优化,其实是在深入了解各个方向,比如 http工作机制、缓存机制、浏览器工作原理、工具链的优化策略、前端框架的原理之后总结出来的一个方案。所以我们每天都在谈的的性能优化其实是一个深不见底的池子,他需要你有完善的知识体系以及丰富的经验,绝不是背背优化点就能搞明白的。
如此一来,我们以后在谈起性能优化,就不要在说这些优化点这些庸俗的东西。而是要深究他背后的原理、和总结出来这个方案的思考。 接下来,我们一个个来攻克。
关于浏览器的工作原理这块之前写过一篇文章,基本涵盖到了一些细节知识点 重学前端(三)-聊聊我们的浏览器的那些事
如上图所示,他其实就分为这么几步
整个的过程就是从 URL 转换为 Bitmap 的过程,先发送请求到服务器,然后服务器返回 HTML,浏览器解析 HTML,然后构建 DOM 树,计算 CSS 属性,然后进行排版,最后渲染成位图,然后经过操作系统或硬件的 API 完成视图的显示。
那么在这几个步骤中,有一个layout 和render 两个步骤,其实也就是布局(回流)和绘制,这也是浏览器关键渲染路径中两个非常重要的步骤,而且非常消耗浏览器资源。而我们的性能优化其实就可以在这两个步骤中做文章。
对于布局而言,我们需要改变的其实是元素的几何信息,比如宽高,和位置,接下来我看看,有哪些操作可以出发布局,这样一来是,就有可能在我么的代码中去避免这个操作。从而达到性能优化的目的
-
添加删除元素
-
操作styles
-
display:none
-
offsetLeft、scrollTop、clientWidthd等
-
移动元素位置
-
修改浏览器大小、字体大小等
我们还是来看哔哩哔哩的火焰图。紫色的部分就是layout。而在布局的过程中有一个非常经典的问题,叫做布局抖动,从而导致页面显得非常卡顿,其实所谓布局抖动就是连续发生layout 过程导致的
而我们怎样能在保证效果的同时在这一步去做性能优化呢?
1、避免回流
比如修改元素位置,那么我们可以使用css动画去解决,利用复合步骤去解决问题,在比如利用vdom 最小限度的去改变元素的布局,
2、读写分离
其实就是利用浏览器的api--requestAnimationFrame去在当前针读数据。下一帧写数据,这样就能达到读写分离的效果了,在社区上有一个fastdom的库就能给我们解决这个问题。
2、避免重绘 对于绘制而言,它只是影响元素的外观,风格,而不会影响布局的,比如background-color。
上图所示,绿色部分就是绘制步骤,而浏览器为了提高新能,在绘制的步骤上开发了复合线程,他就类似于ps 的图层,浏览器也将一些盒模型分为一个个图层,这样一来,修改一些图层,并不会影响其他页面的绘制个布局。
如上图所示,就是复合过程,他引起了样式计算,但是去没有走重绘,而是一个composite layers 过程,
那么我们怎样尽量的使用复合,而避免重绘呢?
- 利用will-change属性创建新的图层
- 如果页面需要有动画效果,尽量使用css3动画,比如transform、opacity 等
3、减缓高频事件的触发 在复杂的网页交互中,比如拖动,滚动,高频点击,他的触发频率非常高,远远的高于60hz 所以,我们就在想真的有必要吗?所以,我们就需要有防抖、节流函数等来帮助我们减缓高频事件!他的原理也很简单,其实就是利用定时器,来延时或者间断的处理事件回调
4、利用浏览器api减少页面抖动
我们知道react16有个fiber的架构,他就是在底层运用浏览器的api requestIdCallback 来实现任务调度,从而最大限度的解决了页面卡顿问题。那么我们在解决卡顿问题的时候,是不是也可以考虑使用这些requestIdCallback、requestAnimationFrame等这些api呢!
在框架横行的今天,react、vue、angular 三分天下,但是在框架的编程范式下,我们往往忽略了自己代码层面的性能优化,总认为框架的作者会考虑这些问题,比如react fiber。然而,我们不知道的是作为一个框架,让你的代码具有可维护性,和这个框架具有普适性,从而推广开来是非常重要的,所以,框架给我们的保证是,在不需要手动优化的情况下,依然可以给提供过得去的性能。而不是非常好的性能,他其实本质上也是操作dom,只是这个事情框架给你做了,你只需要描述你的目的即可
举个例子:
如上图,我最近的vue项目中,就有着严重的性能瓶颈,我们可以看到,在首次渲染之后,有两个非常长的长任务在阻塞页面渲染,显得非常卡顿,他其实本质原因就是,table表格渲染,这种大数据量的渲染是非常容易出现性能瓶颈的。虽然vue 有虚拟dom 和diff 算法兜底,但是,他们也不是免费的,也有着不小的开销。所以,这时候就需要我们去手动优化,比如加入虚拟滚动。
接下来还是从底层去理解,js 执行的开销到底在哪?
如上图所示,js APP.js这个文件的的编译、解析时间有700多毫秒。
而再往后看的我们发现除了有编译、解析脚本还有gc(垃圾回收)都有不少耗时。如此,我们就有办法在这个步骤中去做文章,
- 减少资源大小,对资源做压缩,达到代码减重(tree shaking)这样就能在让解析的内容变小
- 代码拆分(code splitting),按需加载,用到这段代码才加载,这样就能防止无用的js 执行阻塞页面
- 避免过大的行间脚本,由于行间脚本浏览器在解析时无法做优化,所以我们必须减少行间脚本的体积
- 写出迎合浏览器的优化代码
接下来我们逐一解析
前三个,都是一些比较明显的问题,就不再赘述,我们重点解释应该怎样写出迎合浏览器的代码呢?
我们知道,在浏览器中,js的解析引擎叫做v8,其实呢,v8在底层解析的时候,是做了很多事情的,比如,我们知道tcp 是流的传输,于是v8就有了脚本流的优化,解释起来大致意思就是,v8会给我们做预解析,在脚本还没下载完成之后就开始解析代码,在比如,字节码缓存,以及懒解析,而我们要做的就是,就是去迎合浏览器,比如:
- 1、同类型的变量相加,解析时间会更少。
- 2、由于函数默认懒解析,在需要快速解析的时候,在函数声明外加入括号
- 3、重复执行相同方法的代码会比每次运行不同的方法的代码更快(抽象封装的重要性),
- 4、总是以相同的顺序实例化你的对象属性,避免隐藏类调整,实例化后尽量避免添加成员
- 5、避免读取超过数组的长度,以及避免类型转换
- 6、在html层面,减小iframes的使用,避免使用table布局,避免节点深层嵌套,引用脚本尽量使用外链
- 7、css层面,降低css的加载对css 的渲染阻塞,利用gpu加速渲染
之前在代码层面上,我们列出了可以做的优化,然而它带来的收益甚微,可谓微不足道,其实,你仔细看一些vue、element 等知名的开源库,他们没有一个执行上述的优化手段的,因为,他们需要为了可维护性,来牺牲少许的性能是非常划算的。那么我们在写代码时候,其实也需要做一些平衡以及取舍,你可以为了可维护性等理由,而放弃这些优化手段,但是,你却要必须知道,为啥要使用这些优化手段,这样才能在面试,以及将来某个项目中能合理的用上。
而我们在资源方面优化,他是结结实实能看的见的都东西,并且能看到确切的效果。比如文件资源的压缩与合并,那图片格式浏览器解析最快,不需要立即加载的图片能不能懒加载,字体会不会影响性能?
我们知道在网络协议层面,资源越小,那么就代表他的传输时间越少,于是,我们就必须要在文件资源的层面去做一些优化,其实不管怎么优化,他们总遵循这一个原则:
- 1、减少http的请求次数
- 2、减少文件资源的请求大小
那么围绕这两点,其实已经有了很多经优化手段了,我说几种,很多老前端,指定是难以忘怀,只不过在由于网速和技术的进步,他们被淹没在历史的长河中,比如雪碧图,使用gulp 压缩合并html、css 、js 资源,使用imagemin 优化图片大小。,当然这些问题其实,在工程化的今天,他其实是掩盖了这些优化,我们只需要专注开发,描述目的即可,但是有一些,问题我们还是要注意:
- 1、在大图片资源选用的时候,jpg最为合适,图片压缩比高,画质不错,大小不大
- 2、图片等一些资源不需要首屏加载的内容实行懒加载的方案,这是不管工具链发展到什么程度,都需要我们自己去优化的
- 如果使用特殊字体,为了防止加载字体资源的问题,应该使用font-display 有限使用降级的默认方案。让字体先显示
在上文中,我们讲了一些优化方案,但是不管什么方案,他都绕不开工具链的合理使用,这也是我们的性能优化中不可绕开的一个环节,因为工具的使用得当,能兼顾可维护性,和比较好的性能,那么提起工具链,就绕不开新一代的构建工具,webpack、rollup等。今天我们就来探讨一下webpack这个老牌构建工具。
其实要说起的初衷其实是为了开发者能使用上一些语言的新性能又让浏览器能运行开发者编写的代码而起到的中间人的一个身份。只不过由于本身的强大的插件和loder能力,顺带的给我们的性能优化做了。具体的优化方式请移步我之前的文章webpack优化解决项目体积大、打包时间长、刷新时间长问题!
这个手段是收益最大的一个手段,上述的手段其实能力有限,而在传输层面做文章,其实才是事半功倍的的手段
我们知道,虽然我们的代码压缩了,但是他不是个压缩包啊,于是gzip横空出世,那么我们就来看看gzip是个啥?
但是不是每个浏览器都支持gzip的,如果知道客户端是否支持gzip呢,请求头中有个Accept-Encoding来标识对压缩的支持。客户端http请求头声明浏览器支持的压缩方式,服务端配置启用压缩,压缩的文件类型,压缩方式。当客户端请求到服务端的时候,服务器解析请求头,如果客户端支持gzip压缩,响应时对请求的资源进行压缩并返回给客户端,浏览器按照自己的方式解析,在http响应头,我们可以看到content-encoding:gzip,这是指服务端使用了gzip的压缩方式。
使用方式也非常简单,在一些nginx 、node、等web服务器上启用即可。
通常一个网页可能会有很多组成部分,除了文本内容,还会有诸如:js、css、图片等静态资源,有时还会异步发起AJAX请求。只有所有的资源都加载完毕后,我们看到网页完整的内容。然而,一个网页中,可能引入了几十个js、css文件,上百张图片,如果每请求一个资源,就创建一个连接,然后关闭,代价实在太大了。
基于此背景,我们希望连接能够在短时间内得到复用,在加载同一个网页中的内容时,尽量的复用连接,这就是HTTP协议中keep-alive属性的作用。
而在http1.1之后,keep-alive默认开启,也就是我们不用特殊关注他了。
如上图所示,就是一个http 缓存的流程图,在我看来,所有的优化手段都比不上缓存所带来的体验,他直接省去了一些静态文件的请求资源的开销,从而在第二次请求是带来质的提升。而他遵循的规律只有两点:
- 1、对于强制缓存,服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行协商缓存策略。
- 2、对于协商缓存,将缓存信息中的Etag和Last-Modified通过请求发送给服务器,由服务器校验,返回304状态码时,浏览器直接使用缓存。
而我们还可以将缓存和工具链连接起来,从而给用户一个更好的体验,举个例子:webpack在打包时候,可以监听文件是否变化,从而如果文件变化,将改变当前文件名的hash值,其余不变,如此一来,在部署升级之后,用户也只请求到变动文件,从而减少资源的下载。从而达到性能的最优。
它有着两个特点
- 1、加速重复访问
- 2、离线支持
然而不幸的时,在2021年的今天,他的兼容性还堪忧。所以没有普及。
srr技术其实是一个非常老牌的技术,他其实很早都存在,只不过由于vue 、和react 的大火、将传统的ssr革新掉,让前端也能参与到srr的这个浪潮中来。他的原理其实非常简单,就是将首屏内容在服务端拼接为字符串,放在客户端解析。从而减少了客户端的js执行时间。快速渲染页面。达到性能优化的目的
在上述所有的问题搞定之后,我们在文章的开头,提出的最后两个问题,也就清晰了,原理方面其实也是阐述了个大概。
其实在每个方向上都有值得深挖的知识,这些需要被深挖的知识在时时刻刻提醒我们:我们真的很菜,然而,我经常发现,很多人真的只是流于表面,他享受这互联网带来的红利和错觉,误认为自己很强,并且总爱指点江山。在经历了很长时间的思考以后,我问自己,如果被过渡抬高的互联网行业热度一过。我还剩下什么,其实也就会用vue、react的api而已。
故记录此文,并朝着各个方向慢慢攻克,希望给大家在知识体系上添砖加瓦是不对之处。请批评指正!