前端性能优化基础

407 阅读14分钟

前端的性能优化是一个庞大繁杂的领域,涉及到许许多多方面,包括网络传输层面、客户端加载渲染,乃至服务器端的优化等等。这些大的方向下,又有非常多小的分支。所以,前端的性能优化可以说是许多杂项的集合,而且目前来说,并没有非常成体系的这样一套东西。
本文基于掘金小册《前端性能优化原理与实践》,是自己的学习总结,并在此基础上做了一些个人的拓展和提炼,用于形成自己的前端性能优化的相关知识地图。

一、构建工具的优化(Webpack)

webpack的优化主要有两个方向:
1.webpack的构建时间太长
2.webpack打包后的体积过大
针对这两个问题,可以使用相应的配置,或者插件来进行优化,总结如下:
a.使用exclude配置选项,排除非必要的编译文件。
b.配置babel-loader的loader:'babel-loader?cacheDirectory=true',将编译结果缓存至文件系统。
c.使用DllPlugin插件打包第三方库,这个插件会将第三方库打包到单独的文件中,这个文件就是一个单独的依赖库,这个依赖库不会跟随业务代码一起重新打包,只有当依赖自身发生变化的时候才会重新打包,从而加快了打包的速度。
d.使用Happypack插件将loader由单进程转为多进程。
e.使用webpack-bundle-analyzer查看打包后chunk的组成以及依赖,进行相应的优化。
f.使用webpack自带的Tree-shaking功能进行模块级别的冗余代码的删除优化。
g.使用UglifyJsPlugin插件更细粒度的删除冗余代码,包括console、注释等。
h.使用webpack的按需加载功能。

#Gzip压缩原理

在请求头(Request Headers)中添加accept-encoding:gzip;可以开启Gzip压缩。
Gzip压缩通常能够减小70%左右的体积。它的原理是在一个文件中找出那些重复出现的字符串,临时替换它们,从而使整个文件变小。

二、图片相关优化

当下web应用比较广泛的图片格式有:JPEG/JPG,PNG,WebP,Base64,SVG等。
在计算机中,像素用二进制位数来表示。一个像素对应的二进制位数越多,它可以表示的颜色种类就越多。一个二进制位表示两种颜色:0|1,对应:黑|白。如果一种图片格式对应的二进制位数有n个,那么它就可以呈现2^n种颜色。

#JPEG/JPG

JPG最大的特点是有损压缩。JPG压缩的体积小,加载快,但是不支持透明。
JPG适用于呈现色彩丰富的图片。使用JPG呈现大图,既可以保住图片的质量,也不会带来过于头疼的图片体积。是当下比较推崇的方案。

#PNG-8与PNG-24

PNG的特点是无损压缩,质量高,体积大,支持透明。
8位的PNG最多支持256种颜色(2^8),24位的则可以呈现约1600万(2^24)种颜色。 PNG一般用于呈现小的Logo,颜色简单且对比强烈的图片或背景等。

#SVG

SVG(可缩放矢量图),是一种基于XML语法的图像格式。
SVG的特点是文本文件、体积小、不失真、兼容性好。
但是它也有两个明显的局限性:a.渲染成本比较高,对性能不利;b.SVG存在其它图片格式没有的学习成本(它是可编程的)。

#Base64

Base64的特点是文本文件、依赖编码、小图标解决方案。
将Base64格式直接写入img标签的src属性,浏览器可以解析并呈现图片,从而节省了通过路径形式所需要的http请求。Base64是作为雪碧图的补充而存在的。
经过Base64编码后,图片大小会膨胀为原来的4/3,所以对于大图来说,无法使用这种格式。我们可以在webpack中使用url-loader对特定大小的图片进行Base64编码。

#WebP

WebP的特点是图片质量好、支持透明、可以像gif一样支持动态。
但是,虽然集众多优点于一身,WebP也有两个比较明显的缺点:
a.兼容性不好(目前只有Chrome支持比较好)
b.会增加服务器的负担

三、浏览器缓存机制

浏览器缓存机制有4个方面,按照获取资源时请求的优先级排列如下:
1.Memory Cache
2.Service Worker Cache
3.HTTP Cache
4.Push Cache

#HTTP缓存机制

HTTP缓存分为强缓存和协商缓存。命中强缓存失败的情况下,才会走协商缓存。
强缓存
强缓存是用http头中的Expires和Cache-Control两个字段来控制的。
强缓存中,当请求再次发出时,浏览器会根据其中的expires和cache-control判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通信。强缓存命中返回的HTTP状态码为200。
expires是一个时间戳,接下来如果我们试图再次向服务器请求资源,浏览器就会先对比本地时间和expires的时间戳,如果本地时间小于expires设定的过期时间,那么就直接去缓存中取这个资源。
Cache-Control可以视作是expires的完全替代方案。在当下的前端实践里,我们继续使用 expires的唯一目的就是向下兼容。
在 Cache-Control 中,我们通过max-age来控制资源的有效期。max-age不是一个时间戳,而是一个时间长度(以秒为单位)。当Cache-Control与expires同时出现时,我们以Cache-Control为准。
Cache-Control相应的配置属性有: max-age=3600|s-maxage=31536000|public|private|no-store|no-cache
客户端中我们只考虑max-age。
s-maxage仅在代理服务器中生效,s-maxage就是用于表示cache服务器上(比如 cache CDN)的缓存的有效时间的,并只对public缓存有效。
资源设置了public,那么它既可以被浏览器缓存,也可以被代理服务器缓存;如果我们设置了private,则该资源只能被浏览器缓存。private为默认值。
no-cache绕开了浏览器:我们为资源设置了no-cache后,每一次发起请求都不会再去询问浏览器的缓存情况,而是直接向服务端去确认该资源是否过期。
no-store顾名思义就是不使用任何缓存策略。在no-cache的基础上,它连服务端的缓存确认也绕开了,只允许你直接向服务端发送请求、并下载完整的响应。
协商缓存
协商缓存依赖于服务端与浏览器之间的通信。
协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。 如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是304。
协商缓存的实现:从 Last-Modified到Etag Last-Modified是一个时间戳,如果我们启用了协商缓存,它会在首次请求时随着Response Headers返回。随后我们每次请求时,会带上一个叫If-Modified-Since的时间戳字段,它的值正是上一次response返回给它的last-modified值。
Etag 是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的Etag就是不同的,反之亦然。因此Etag能够精准地感知文件的变化。
Etag 的生成过程需要服务器额外付出开销,会影响服务端的性能,这是它的弊端。

#Memory Cache

Memory Cache是指存在内存中的缓存。从优先级上来说,它是浏览器最先尝试去命中的一种缓存。从效率上来说,它是响应速度最快的一种缓存。
内存缓存是快的,也是“短命”的。它和渲染进程“生死相依”,当进程结束后,也就是tab关闭以后,内存里的数据也将不复存在。

#Service Worker Cache

Service Worker 是一种独立于主线程之外的Javascript线程。它脱离于浏览器窗体,因此无法直接访问DOM。

#Push Cache

关于Push Cache,只需要了解以下三点即可:
1.Push Cache 是缓存的最后一道防线。浏览器只有在Memory Cache、HTTP Cache和 Service Worker Cache 均未命中的情况下才会去询问 Push Cache。
2.Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
3.不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache。

四、本地存储

本地存储相关的主要有:
1.Cookie
2.Local Storage,Session Storage
3.客户端的非关系型数据库:IndexDB

五、CDN的缓存与回源机制

CDN(Content Delivery Network,即内容分发网络)指的是一组分布在各个地区的服务器。这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。
CDN的核心点有两个,一个是缓存,一个是回源。
缓存就是把资源拷贝一份到CDN服务器上这个过程。
回源就是发现CDN上没有这个资源(或者说资源过期了),转头向根服务器请求这个资源的过程。
同一个域名下的请求会不分青红皂白地携带Cookie,而静态资源往往并不需要Cookie携带什么认证信息。把静态资源和主页面置于不同的域名下,完美地避免了不必要的Cookie的出现。

六、服务端渲染原理

客户端渲染:页面上呈现的内容,在HTML源文件里往往找不到,这是客户端渲染的一个特点。
服务端渲染:服务端渲染的模式下,当用户第一次请求页面时,由服务器把需要的组件或页面渲染成HTML字符串,然后把它返回给客户端。客户端拿到手的,是可以直接渲染然后呈现给用户的HTML内容,不需要为了生成DOM内容自己再去跑一遍JS代码。页面上呈现的内容,我们在HTML源文件里也能找的到。
服务端渲染往往是处于效益的考虑,而不是性能。搜索引擎只会查找页面内现成的内容,而不会跑JS文件。为了把现成的内容展示给搜索引擎,需要启用服务端渲染,增强所谓的SEO。
服务端渲染还能很好的解决首屏渲染的问题。
服务端渲染本质上是本该浏览器做的事情,分担给服务器去做。这样当资源抵达浏览器时,它呈现的速度就快了。

七、浏览器背后的运行机制

浏览器内核决定了浏览器解释网页语法的方式,浏览器内核可以分成两部分:渲染引擎和JS引擎。
渲染引擎包括了HTML解释器、CSS解释器、布局、网络、存储、图形、音视频、图片解码器等零部件。
市面上常见的浏览器内核有四种:Trident(IE),Gecko(火狐),Blink(Chrome,Opera),Webkit(Safari)。

#浏览器渲染原理

渲染过程:简单来说,渲染引擎根据HTML文件描述构建相应的数学模型,调用浏览器各个零部件,从而将网页资源代码转换为图像结果,这个过程就是渲染过程。
a.HTML 解释器:将HTML文档经过词法分析输出DOM树。
b.CSS 解释器:解析 CSS 文档, 生成样式规则(CSSOM)。
c.图层布局计算模块(Layout):布局计算每个对象的精确位置和大小。
d.视图绘制模块(Paint):进行具体节点的图像绘制,将像素渲染到屏幕上。
e.JavaScript引擎:编译执行Javascript代码。
渲染过程说白了,首先是基于HTML构建一个DOM树,这棵DOM树与CSS解释器解析出的CSSOM 相结合,就有了布局渲染树。最后浏览器以布局渲染树为蓝本,去计算布局并绘制图像,我们页面的初次渲染就大功告成了。

#CSS优化

需要知道的是CSS引擎查找样式表,对每条规则都按从右到左的顺序去匹配。CSS的优化可以遵循下面的几项规则。
1.避免使用通配符(*),只对需要用到的元素进行选择。
2.关注可以通过继承实现的样式,避免重复匹配重复定义。
3.少用标签选择器。
4.减少嵌套,选择器深度最好不要超过三层。

#浏览器的阻塞

浏览器在构建CSSOM的过程中,不会渲染任何已处理的内容。CSS资源是阻塞渲染的资源,需要将它尽早、尽快地下载到客户端,以缩短首次渲染的时间。
1.尽早:将CSS放在head标签里。
2.尽快:启用CDN实现静态资源加载速度的优化。
JavaScript的作用在于修改,修改网页的方方面面:内容、样式以及如何响应用户交互。JS执行会阻塞CSSOM,也会阻塞DOM。
JS引擎是独立于渲染引擎存在的。我们的JS代码在文档的何处插入,就在何处执行。 当HTML解析器遇到一个script标签时,它会暂停渲染过程,将控制权交给JS引擎。JS 引擎对内联的JS代码会直接执行,对外部JS文件还要先获取到脚本、再进行执行。等JS引擎运行完毕,浏览器又会把控制权还给渲染引擎,继续 CSSOM 和 DOM 的构建。
因此与其说是JS把CSS和HTML阻塞了,不如说是JS引擎抢走了渲染引擎的控制权。浏览器之所以让JS阻塞其它的活动,是因为它不知道JS会做什么改变,担心如果不阻止后续的操作,会造成混乱。
JavaScript的三种加载方式:
1.正常模式,哪里加载,哪里执行。
2.async模式。async模式下,JS不会阻塞浏览器做任何其它的事情。它的加载是异步的,当它加载结束,JS脚本会立即执行。
3.defer模式。defer 模式下,JS的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded事件即将被触发时,被标记了defer的JS文件才会开始依次执行。 从应用的角度来说,一般当我们的脚本与DOM元素和其它脚本之间的依赖关系不强时,我们会选用async;当脚本依赖于DOM元素和其它脚本的执行结果时,我们会选用defer。

八、Event Loop与异步更新策略

事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。
常见的macro-task比如:setTimeout、setInterval、setImmediate、script(整体代码)、I/O 操作、UI 渲染等。
常见的micro-task比如: process.nextTick、Promise、MutationObserver等。

九、回流(Reflow)与重绘(Repaint)

回流:浏览器需要重新计算元素的几何属性,然后将重新计算的结果绘制出来。
重绘:浏览器不需要重新计算元素几何属性,布局位置没有变。

十、性能监测

#Chrome devtools Performance的使用
#LightHouse插件的使用(可用于生成页面性能分析报告)
#可编程的性能上报方案:window.performance

十一、其它常用优化方案

#图片的Lazy-Load
#事件的节流(throttle)与防抖(debounce)的应用