下面的文字在翻译时做了些调整和补充,并不完全忠实于原文。有很多我自己也没有深入学习的点,翻译也不能保证准确,所以有时间还是看原文。阅读时需要注意区分加载性能和运行时性能的不同策略。文章涵盖很多的细节,实践中从识别出性能瓶径开始展开性能优化会更好。可以多了解下Chrome DevTools的使用。比如:
* Optimise your web development workflow,视频
* Measure Resource Loading Times 打开后,左侧还有关联的几篇。
* 浏览器渲染性能优化
通常情况下需要避免微改进,虽然它能帮助不断优化性能,但它无法聚焦。我们需要使用可衡量的目标来贯穿整个开发流程。下面例出一些不同的模型,其中不乏片面的观点,但早点定义出来还是必要的。
设置目标
1.超越竞争对手20%
以前的研究表明,如果让用户感觉到性能差异,必须提高20%以上。所以我们定义性能优化的目标也不要低于这个值。页面的性能有几个不同的指标:
* 页面加载完成时间 (Full-Page loading)
* 开始渲染时间 (Start Rendering Time,使用WPT测试)
* 首次有效渲染时间 (First-meaningful paint, Chromium定义)
* 可交互时间 (Time to interactive)
首次有效渲染时间和可交互时间都可以使用Lighthouse进行测试,根据用户情况选择适当的测试环境,包括机型和网络环境。输出的样式如下:
如果你愿意,也可以收集这些数据,制成表格,从中去除20%再设定出你的目标(如性能预算表(Performance budgets))。这时,如果你想通过减小JS大小来改进可交互时间,就会更加有方向了。
Performance budget builder by Brad Frost
把表格同你的伙伴分享,可以让大家都能留意到对性能的影响。
2.100ms响应时间和60fps的帧率
RAIL性能模型给出建议是:从用户操作(input)到页面做出反应(feedback)应当在100ms以内,这样页面的处理必须在50ms内完成。而对于动画这种耗时的操作,如果控制不住时间还不如什么都不做。保证用户的行为可以得到及时响应,而不是因为被页面的处理操作造成卡顿的感觉。
另外为了60fps的帧率,每帧的绘制时间应当在16ms内(最好在10ms内)完成。一些好的优化实践或者好好利用空闲时间可以做些优化。这些是运行时性能的目标,不是加载性能。
3.首次有效绘制时间低于1.25s,SpeedIndex低于1000
理想的性能目标是,在较好的网络条件下,开始渲染时间应当低于1s,首次有效渲染时间最长也要低于1250ms,SpeedIndex低于1000。在移动端,3G网络下开始渲染时间如果在3s还是可以接受的。
定义环境 (DEFINING THE ENVIRONMENT)
4.选择你的开发工具
这里似乎不用特别说明,大家都有自己的见解。
5.渐进增强 (Progressive Enhancement)
在架构和发布上要保持渐进增强才能立于不败之地。先完成核心体验,再为不同的浏览器完成更为高级的功能,以此创建出弹性的用户体验(Resilient Web Design)。
6.Angular, React, Ember…
熟悉一个支持服务端渲染的框架。在选择一个框架前,最好确认下服务端渲染和客户端渲染的差异。如果发现性能问题,也很难变更所使用的框架了。对于常使用的JS框架,一定要有广泛的了解,再做好充分的评估。
当创建一个Web应用时,不妨了解下PRPL模式和应用框架(Application shell)架构。
PRPL代表的是:拉取关键资源,渲染首屏,资源预取,按需加载。
一个应用框架(Application-Shell)就是最简化的HTML,CSS和JS。
7.AMP或是Instant Articles?
你或许会考虑使用Google AMP或是Facebook的Instant Articles。AMP提供了一个稳健的性能框架,而Instant Articles可以使用在Facebook上体验上更好的性能。
当然你也可以考虑下渐进式Web AMPs。
8.好好选择CDN
无论什么页面,总可以其中一部分转改为静态资源(Static Site Generator),通过CDN部署,避免数据库请求。
你也可以选择基于CDN的static-hosting平台,再通过一些交互组件来增强(JAMStack)。
CDN也是可以提供动态内容的,并不需要只限定于静态内容。选择CDN时可以确认下是否会做压缩、转换、HTTP2,组装静态和动态内容等内容。
优化
9.设置优先级
先要弄清出从哪入手。可以盘点下页面中的资源(JS,图片,字体,第三方脚本和其它较重的模块,如轮播(Carousel)组件, 复杂的图文,以及多媒体资源),将它们分类列出。并区分出属于哪类需求:
* 核心体验 (Core experience)
* 增强体验 (Enhanced experience)
* 额外体验 (Extras experience, 可以进行懒加载)
可以参考这个文章中的实践进行优化。
10.使用cutting-the-mustard技术
使用cutting-the-mustard技术在老旧浏览器拥用核心体验,同时在现代浏览器上达到更好的用户体验。对应限制资源加载:
* 立即加载核心体验所需要的资源。
* 在DomContentLoaded后加载增强体验所需要的资源。
* 在load事件为额外体验加载其它资源。
注意通过浏览器版本判断设备能力的方法并不准确,而且现在也没有更好的方法。
11.考虑使用微优化(micro-optimization)和渐进式启动(progressive booting)
一些应用在可以渲染页面之前,或许需要一些时间来进行初始化。这时不要显示加载提示,那个没有任何意义。最好能显示出一个基本的样子(skeleton screens)出来。
应用一些为初始化时间提速的技术,如tree-shaking和code-splitting。还可以使用一个前置编译器来通过服务端降低客户端渲染的耗时,尽快显示有意义的内容。
最后,还可以考虑使用Optimize.js来实现快速初始化(但可能不再需要了)。
渐进式启动(Progressive booting)就是先通过服务器端渲染快速完成首次有效渲染,再通过最小的JS让可交互时间接近于首次有效渲染时间。
客户端渲染还是服务器端渲染呢?通过渐进式启动(Progressive booting)就是先通过服务器端渲染快速完成首次有效渲染,再通过最小的JS让可交互时间接近于首次有效渲染时间。然后根据需要加载其它内容。不幸的是,Paul Lewis却指出这个实践很难通过现有的框架实现。但你仍然可以考虑用它完成启动速度的极致优化。
12.HTTP cache的响应头有没有设定好
这一点很容易被忽略。一定要确认expires
, cache-control
, max-age
等这些http cache相应的响应头都设定正确了。
可能的话,就为静态资源使用cache-control: immutable
,避免不必要的验证(目前还只有Firefox针对https页面支持)。详细的内容,可以参考:
Heroku的Primer on HTTP caching headers, Jake Archibald的Caching Best Practices, 以及Ilya Grigorik的HTTP caching primer。
13.限制第三方库的使用,异步加载JS
因为浏览器渲染页面的流程,JS资源会阻塞渲染流程。对于一些不会影响首屏渲染的JS可以使用defer
和async
属性。
在实践中,如果需要考虑IE9之前用户,建议使用async
。另外要限制第三方库和脚本的使用,特别是分享按钮和<iframe>
中引用的。可以使用静态分享按钮(比如SSBG)和静态链接(static links to interactive maps)的方式来解决。
14.优化图片
尽量使用响应式图片和<picture>
元素。还可以利用<picutre>
加上JPEG回退处理来使用WebP格式的图片。对于UC浏览器也可以使用H.264格式图片。Sketch直接支持WebP, 也可以在Photoshop中使用插件导出,或者其它方案。
Responsive Image Breakpoints Generator可以自动处理图片,并生成标签。
也可以尝试使用client hints,增加更多的设备信息,让服务器返回不同的图片请求。需要注意它对CDN的影响。工具Responsive Image Breakpoints Generator或者Cloudinary也可以帮助简化你的工作。大部分情况下,单独使用srcset
和sizes
也能有很不错的效果。
另外在Smashing Magazine, 在图片命名时使用-opt
后缀,如brotli-compression-opt.png
,所有人都会知道这个图片已经被优化了。
15.进一步优化图片
如果页面需要快速的加载图片,可以:
* 使用Progressive JPEG,并且使用mozJPEG压缩一下,它能够通过处理扫描层次来改善启始渲染时间。
* 使用Pingo处理PNG图片。
* 使用Lossy GIF处理GIF图片。
* 使用SVGOMG处理SVG图片。
去掉图片一些不必要的部节,比如缩略图,EXIF信息,或者应用高斯模糊过滤下,这样可以降低图片大小 (那些数据有时比你想像的要大很多!)。必要时可以降色,甚至是使用黑白图片。对于背景图,在导出图片时以0~10%之间的品质输出也是可以的。
可以参考了解更多的内容:
* Improving Perceived Performance with Multiple Background Images
* How Medium does progressive image loading
* Tiny Thumbnails
* The “Blur Up” Technique for Loading Background Images
16.优化字体文件了吗?
这个主题并不太适用于中文环境,可以做个了解。如果使用中文字体,需要特别慎重,它们的字体文件都是以MB为单位的。
概括一下要点:
* 使用WOFF2。
* 参考Comprehensive Guide to Font-Loading Strategies并且使用Serivce Worker - Cache来缓存字体。
* 要快速有效,可以参考Pixel Ambacht的方法。
* 如果依赖于第三方提供的字体,可以使用Web Font Loader,FOUT会优于FOIT。
* 异步的字体加载可以使用loadCSS。
* 使用系统字体代替。
* 网页字体优化。
再给一个中文字体的参考:
* Web 字体的选择和运用
17.及早推送关键的CSS
把首屏需要的样式,即关键的CSS(Critical CSS), inline到页面里已经是常用的实践,但本质上它只适用于首次加载显示的场景(Why inlining everything is not answer!)。在移动网络下,要综合主文档的大小来决定内联CSS的策略,因为内联CSS增大了主文档反而可能会使得主文档的首次有效渲染的耗时变长,特别是忽略了浏览器本身的Memory Cache和Http Cache的作用。(所以对于任何教条式的规则一定要区分它所针对的场景!!)
综合考虑加载的耗时,关键CSS的大小最好保持在14KB,注意这是PC上的数据!如果是移动网络,不要让主文档太大,总之越小越好。有两个工具可以帮助完成:CriticalCSS和Critical。
也可以使用Filament Group的conditional inlining approach。
如果使用HTTP/2,关键CSS可以存在多个CSS文件,通过服务器推送到达客户端,不过可能遇到一些缓存上的坑,参考Hooman Beheshti幻灯片中第 114页。但是服务器推送还是比保持连接更为有效,可以考虑创建一个可感知缓存状态的服务器推送机制。已经有一个新的cache-digest
规范尝试从标准层解决这个问题了。
18.使用Tree-shaking和Code-splitting减少加载的流量
Tree-shaking能够帮你清理掉生产环境中不需要的代码,也可以使用Webpack 2去掉不需的模块输出。使用UnCSS或者Helium清除掉不再使用的CSS。另外,你可能还会考虑下如何写出更有效的CSS选择器,以及如何避免引入复杂的样式。
Webpack的另一个特性, Code-Splitting, 可以将代码分段以便按需加载。它可以帮助你做到减少初次下载,其余代码则按需加载。
还是 Rollup比Browserify的导出有更好的表现。使用Rollupify还可以将ECMAScript 2015转成一个大的CommonJS模块,因为小的模块会有更大的性能开销。
19.改进渲染性能
将渲染开销较大的组件使用CSS containment隔离开来,比如限定样式的作用域,或者第三方插件(widget)等。确保在动画或者滑屏时不会有卡顿。如果实在做不到,可以让页面交错达到60~15的帧率。使用will-change
来通知浏览器哪些元素的哪些属性会发生变化。
另外,最好还是通过Chrome DevTools来全面的评估渲染性能。可以从Paul Lewis的免费课程开始。另一个不错的文章是Sergey Chikuyonok的GPU Animation: Doing It Right。
20.预热网络连接
使用Skeleton screens(先显示基本的框架),并且对开销大的元素执行懒加载,如字体、JS、轮播元素、视频和iframe。也可以使用Resource Hints中定义的一套预处理策略。
* dns-prefetch
DNS预解析
* preconnect
预连接
* prefetch
预加载且会运行(JS)
* preload
预加载,但不会运行
* prerender
预渲染,成本太高不建议使用
实践中并不是需要这些特性混合使用,比如通常preconnect
会比dns-prefetch
更有效。preload
则会比prefetch
和prerender
有更高的收益(更易于使用)。
翻译就到这里了,剩余的部分主要和HTTP/2有关,里面还是有些关键内容的,简单概述如下:
* 有没有使用Brotli或者Zopfli压缩?
* 有没有使用OCSP简化TLS握手?
* 有没有使用HPACK精简响应头?
* 有没有使用Service Worker进行更精细的控制?