Web性能调优一直是高级前端必须掌握的技能,市面上不少书简绍性能调优的书总是告诉读者一些理论性的东西,而如何去实践说的却不多,这本书不仅告诉读者Web性能优化的理论知识,同时还会告诉读者怎么用node去设置,是一本前端进阶必看的书。
理解Web性能
Web性能主要指网站的加载速度。你可以通过提高网站速度来加快内容的传输,从而改善用户的体验。电子商务网站上近一半的用户希望在2秒内完成。如果加载时间超过3秒,40%的用户将会退出。页面相应时间每延迟1秒就意味着7%的用户不再进一步操作。
加载时间是用户请求网站到网站出现在用户屏幕上所经历的时间。
本节从减少传输的数据量入手,简单的简绍了3中提高性能的方法:缩小资源、使用服务器压缩、压缩图像。
缩小(minification)文本资源是从基于文本的资源中去除所有空白和非必要字符的过程,因而不会影响资源的工作方式。
缩小资源
下面命令-o
表示输入的文件路径,通过使用下面命令缩小资源后 CSS文件缩小了14%,JS文件缩小了66%,HTML缩小了19%,缩小的还是挺可观的。
1 | 缩小CSS |
使用服务器压缩
服务器压缩的工作方式是用户从服务器请求网页,用户的请求会附带一个Accept-Encoding
的头信息,向服务器告知浏览器可以使用的压缩格式。如果服务器按照Accept-Encoding
头信息中的内容进行编码,它将用一个Content-Encoding
响应头信息进行回复,其值是所使用的压缩方式。gzip
是比较常用的一种压缩格式,express服务配置gzip
如下。
1 | var express = require("express"); |
通过上述两行代码,压缩后资源缩小了66%。
压缩图像
压缩图像书中简绍了使用常用的TinyPNG去压缩,大小缩小了60%左右。
通过这三种方式,网站的加载速度提高了近70%,还是非常可观的。
使用评估工具
第一个评估工具就是Google PageSpeed Insights,去该网站输入你要分析的网站,它会给你一些优化的建议,当然网页得是已经在线上跑的网页,另外国内需要翻墙。
第二个评估工具是Google Analytics,这个工具比较全面,但是需要在网页中注入JS脚本,如果是大公司的开发者,注入Google的代码往往需要走法律审查,因为安装跟踪代码时,需要接受法律协议条款。
首字节时间(Time to First Byte,TTFB):从用户请求网页到相应第一个字节到达之间的时间。首字节时间往往跟队列请求、DNS查找、连接设置和SSL握手等有关。
页面创建过程:解析HTML以创建DOM -> 解析CSS以创建CSSOM -> 布局元素 -> 绘制页面。
优化CSS
移动优先
移动优先响应式设计:默认样式为移动设备定义,并且随着屏幕宽度的增大而增加复杂性。
桌面优先响应式设计:默认样式为桌面设备定义,并且随着屏幕宽度的减小而降低复杂性。
通常响应式设计中使用移动优先的响应式设计会更好一点,主要的原因有:
1.通常移动设备的处理能力和内存通常低于桌面设备,使用移动优先不需要解析媒体查询。
2.从开发角度出发,扩大样式规模更容易实现。
3.手机用户量激增,搜索引擎对移动设备逐渐更友好。
避免使用@import声明
CSS中可以使用@import来引入一个样式,使用方式如下:
1 | @import url(fonts.css); |
最好不要使用@import
来引入一个样式,因为@import
是串行的,会增加页面的总体加载和渲染时间。可以使用<link>
标签来代替,因为<link>
标签是并行的。
Less中的@import
最终编译到css中的并不是CSS语法中的@import
,所以可以使用。
在<head>中放置CSS
在<head>
标签中放置CSS要比在<body>
标签中放置CSS有两个好处:
- 无样式内容闪烁的问题;
- 加载时提高页面的渲染性能。
如果CSS放在<body>
标签中,如果放在页面HTML结构的下方那么就会先渲染一个没有自定义样式的页面,等加载完CSS以后才会有自定义样式,所以会有无样式内容闪烁的问题。
放在<body>
中还有一个问题是页面加载完<body>
中的样式以后会重新渲染和绘制整个DOM,页面渲染性能较差。
使用CSS过渡
CSS过渡的优点:
- 广泛支持;
- 回流复杂DOM时,CPU的使用效率更高;
- 无额外开销。
如果动画可以使用CSS过渡来实现的话,最好使用CSS过渡而不是JS来改变DOM(减少回流)。
使用will-change来优化过渡
will-change使用方法:
1 | will-change: 属性1, [属性2]... |
will-change可以告诉浏览器哪个属性将会过渡,但是不要使用will-change: all;
。
其他的优化点:
- 使用简写属性;
- 使用CSS潜选择器;
- 分割CSS不加载当前页面中不会显示的CSS;
- 尽可能使用flexbox布局。
关键CSS技术
关键CSS,即折叠之上的内容,这些是用户会立即看到的内容,需要尽快加载。
非关键CSS,即折叠之下的内容,这些是用户开始向下滚动页面之前看不到的内容样式,这种CSS也应该尽快加载,但不能在关键CSS之前加载。
书中的折叠是指屏幕的底部,实际上关键CSS就是首屏样式,非关键CSS就是非首屏的样式。
渲染阻塞指的是阻止浏览器将内容绘制到屏幕的任务活动,这是Web中不可避免的事情。无论使用<link>
还是@import
引入样式都会产生渲染阻塞(虽然<link>
下载是并行的)。
加载首屏样式:为了减少渲染阻塞时间可以直接把关键CSS样式放在<style>
标签中。
加载非首屏样式:非首屏样式也会遇到渲染阻塞的问题,可以使用preload来减少阻塞渲染时间。
1 | <link rel="preload" href="index.css" as="style" onload="this.rel='stylesheet'"> |
响应式图像
通过媒体查询来适配高DPI显示器
1 | /* 正常屏幕 */ |
-webkit-min-device-pixel-ratio
是旧的浏览器对高DPI的支持,如果比率是1相当于是96DPI,2相当于是192DPI;min-resolution
是现代浏览器所支持的直接显示值就行了,最后min-width
根据屏幕的宽度来加载不同大小的图片。
CSS中使用SVG
CSS可以直接把SVG当做图片来使用,实际上本身也可以看成图片:
1 | background-image: url(../img/masthead.svg); |
HTML中传输图片
图片全局max-width规则:在响应式网站中图片往往最大是屏幕的宽度,所以显示最大宽度100%会很有用的。
1 | img { |
商品可以通过媒体查询根据设备分别率来使用不同的图片,实际上H5中,img标签的srcset和sizes属性也可以实现类似的功能。srcset可以根据屏幕的宽度来加载不同的图片。sizes可以通过屏幕的宽度设置图片的宽度,如下。
1 | <img |
上述代码中在512px像素宽度的时候图片是img/amp-small.jpg
,768px像素宽度的时候图片是img/amp-medium.jpg
,1280px像素宽度的时候图片是img/amp-large.jpg
,可见后面的w指的是屏幕宽度为多少。同样的在最小宽度704px的时候图片的宽度是宽度的50%,最小宽度是480px的时候图片的宽度是75%,最小宽度更小的时候图片的宽度是100%。
如果需要在相同的宽度的时候,根据设备分别率来显示不同的图片,那么srcset和sizes就不能做了,此时可以考虑功能更强大的picture标签,如下。
1 | <picture> |
可以使用type属性来加载webp图片,如下。如果支持的话使用图片img/apm-small.webp
,不支持的话使用图片img/amp-small.jpg
。
1 | <picture> |
picture很好用,如果不支持的时候可以走img标签做兜底处理,如果在低版本浏览器也希望使用picture标签该怎么办,就得靠picturefill了,使用方式如下。下面第一个script用来创建一个picture元素,防止因为没有该元素而导致解析错误,第二个script用来异步下载库文件。
1 | <script>document.createElement("picture");</script> |
图像的进一步处理
使用雪碧图
雪碧图的好处:将大量图片缩减为单个图片,可以更加高效地传递资源,并通过减少到Web服务器的连接数来缩短页面的加载时间。
注:使用雪碧图可以减少HTTP请求,但在HTTP2中是反模式。
生成雪碧图:
1 | npm install -g svg-sprite |
将雪碧图回退为图片可以使用这个工具:https://github.com/filamentgroup/grumpicon
缩小图像
书中减少使用imagemin来缩小图片jpeg和png的图片,同时也支持生成webp图片: https://github.com/imagemin/imagemin
imagemin提供了大量的插件:https://www.npmjs.com/search?q=keywords:imageminplugin
使用svgo来压缩svg图片:https://github.com/svg/svgo
使用懒加载
懒加载简单实现:
1 | (function(window, document){ |
上述懒加载的使用:图片上添加lazy样式,同时使用data-src代替src,src使用默认未加载图片来代替,如:
1 | <img src="img/blank.png" data-src="img/red-snapper-1x.jpg" class="recipeImage lazy"> |
更快的字体
将ttf字体转换为其他字体:
1 | npm install -g ttf2eot ttf2woff ttf2woff2 |
CSS中使用自定义字体:
1 | /* 定义字体 */ |
使用unicode-range加载字体子集,如下面把Open Sans Light
拆分成BasicLatin部分和Cyrillic部分,使用unicode-range
定义字符范围,如果文字中只有BasicLatin部分那么只会下载上面的文字,如果只有Cyrillic的文字那么下载下面的文字,都有会都下载。
1 | @font-face{ |
字体下载和字体实际显示直接可能有一段时间,那么这段时间内的字体是怎么显示呢?可以使用font-display来控制:
font-display: auto;
默认值,类似于block。font-display: block;
阻塞文本渲染,直到关联的字体加载完成。font-display: swap;
显示回退文本,加载字体后显示自定义字体。font-display: fallback;
auto和swap的折中方案,短时间(100ms)内显示空白,之后显示回退文本,如果字体加载完后,显示自定义字体。font-display: optional;
几乎和fallback一样,只是浏览器有更大的自由度来控制。
但是font-display
并未得到广泛的支持,使用JS来做回退处理:
1 | (function(document){ |
保持JavaScript的简洁与快速
script标签会阻塞页面的渲染,放在body最后面有助于加快页面加载速度。
script带有async属性与不带async的区别:
不带async:下载脚本->脚本下载完成->浏览器等待其他脚本->执行脚本
带有async:下载脚本->脚本下载完成->执行脚本
带有async脚本下载完会立即执行而不会阻塞渲染。
使用async时需要注意,async下载完会立即执行那么,有可能执行的顺序跟script标签的顺序不同,从而导致JS执行报错。如有一个jquery.min.js文件,还有一个behaviors.js文件,其中behaviors.js引用到jquery.min.js中的$(jQuery对象),那么两个都用async就可能就会在behaviors.js中的$出现没有定义的情况。
解决方法:
1.可以把两个文件文件合并成一个
1 | linux 合并两个文件: |
1 | <script src="js/alameda.js" data-main="js/behaviors" async > |
1 | // js/behaviors.js 中使用AMD模块 |
- 使用defer。
书中还简绍了jQuery的替代方案和用原生JS代替jQury,现在MVVM时代很少用jQuery了,就不简绍了,原生方案可以看这里:https://github.com/nefe/You-Dont-Need-jQuery。
使用Service Worker提升性能
Service Worker在单独的线程上工作,无法访问window对象,但可以通过中介(如postMessage API)间接访问。
使用方式:
- 注册Server Worker
1 | if ("serviceWorker" in navigator) { |
- 编写Server Worker代码
1 | // 文件/sw.js |
微调资源传输
上面在使用gzip的时候使用了compression
中间件,实际上compression
是支持传入压缩等级的 范围是0~9,默认是6,但并不是越高越好,往往默认值的效果是比较好的。
另外compression
也支持压缩特定的资源,可以使用filter,返回为true的时候表示压缩,false不压缩。
1 | app.use(compression({ |
使用Brotli压缩
Accept-Encoding
中如果有br
说明支持Brotli压缩,express可以使用shrink-ray
来开启Brotli压缩。
1 | // 假设你已经运行过: npm install https shrink-ray |
brotli中的quality范围为0~11,值越大文件越小,默认是4,一般也够用了。
设置缓存
设置Cache-Control头部的max-age指令
1 | app.use(express.static(path.join(__dirname, pubDir), { |
响应头会带有:Cache-Control: max-age=10
,注意max-age的单位是秒。
Cache-Control:no-cache
: 向浏览器表明,下载的任何资源都可以储存在本地,但浏览器必须始终通过服务器重新验证资源。Cache-Control:no-store
: 比no-cache
更近一步,它表示浏览器不应存储受影响的资源。要求浏览器每次访问页面时下载所有受影响的资源。Cache-Control:stale-while-revalidate=10
: 与max-age
类似,单位也是秒,当资源过期后仍然使用过期的资源,同时发出请求并缓存新的资源,下次再请求的时候使用新的资源。
在CDN的Cache-Control
有时会与privite
和public
连用,如Cache-Control: privite, max-age=10
,其中privite表示中介(CDN)不在其服务器上缓存资源,public则缓存。
对不同资源设置不同的缓存策略:
资源类型 | 修改频率 | Cache-Control头部值 |
---|---|---|
HTML | 可能频繁修改,但需要尽可能保持最新 | private, no-cache, max-age=3600 |
CSS和JS | 可能每月修改 | public, max-age=2592000 |
图片 | 几乎不会修改 | public, max-age=31536000, |
代码实现:
1 | app.use(express.static(path.join(__dirname, pubDir), { |
资源提示
preconnect、prefetch与preload的使用:
1 | <link ref="preconnect" src="https://code.jquery.com"> |
preconnect可以提供更早的DNS查询,但是如果跟HTML是同域名的时候是没用什么用的,因为已经查询过DNS了,preconnect主要是为了查询其他域名,由于HTML是自上而下解析的,通常把preconnect放在HTML的head中的上面的位置。
prefetch告诉浏览器下载特定的资源,并将其存储到浏览器缓存中。通常用来预取同一个页面的资源,或者优先缓存下一页的资源。缓存下一页的资源使用时要小心,不要下载下页没有的资源,否则会造成过多的请求。
preload如果没有as属性,可能会导致请求2次的情况,另外preload只会缓存本页面的资源。
HTTP2未来展望
HTTP1的问题:
- 队头阻塞:HTTP1无法处理超过一小批的请求(通常认为是6个,因浏览器而异)。请求按接收顺序响应,在初始批处理中的所有请求完成之前,无法开始新的请求。如总共有9个任务,第一批会一次性加载6个,得等这6个中最慢的加载完后才会加载下一批的剩余3个请求。可以通过域名分片(不同域名加载不同批的资源)来处理,但实现起来比较繁琐。
- 未压缩头部:之前zip、br等压缩处理压缩的都是响应体,但是头部信息不能压缩,而有的时候头部信息甚至比响应体更大。
- 不安全网站:HTTP1可以不用实现SSL。
HTTP2对上述问题的处理
- 不再有队头阻塞:HTTP2通过实现新的通信体系结构来并行满足更多请求。新的信道使用一个连接并行处理多个请求,连接的构成:
流是服务器和浏览器之间的双向通信通道,一个连接可以有多个流。
消息由流封装,单个消息相当于HTTP1的一次请求或一次响应。
帧由消息封装,帧是消息的分割符。如响应消息中的HEADERS帧表明下一数据表示响应的HTTP头,响应消息中的DATA帧表示下一数据是所请求的内容。 - 头部压缩:使用了HPACK压缩算法来解决这个问题,不仅压缩头部数据还通过创建一个表来存储重复的头部,以删除多余的头部。
- 强制HTTPS:HTTP2必须实现SSL,因此HTTP2一定是HTTPS。
HTTP2对不支持HTTP2的浏览器的处理:每个HTTP2服务器底层都应有一个HTTP1服务器在等待一个不支持HTTP2的客户端出现。
HTTP2的简单使用:
1 | var fs = require("fs"), |