not a better man

前端技术

让Web站点优化变得容易:GoogleI/O 2018(译)

减肥前VS减肥后

减肥前VS减肥后

原文地址Web Performance Made Easy: Goodle I/O 2018 edition

一直以来,我们都在竭尽全力的优化web站点,为了是web站点加载更快,性能更好。在这篇文章中,我们想要向大家分享新的工具,方法,库来优化web站点性能。首先,我们会展示在开发Oodles Theater App 的过程,我们用到的一些优化手段,然后我们将会谈论预测加载以及使用guess.js的计划。

更喜欢观看本文的视频?

性能优化的必要性

Internet 变得越来越重,如果我们通过state-of-the-web查看相关数据,手机站点中等的网页大小将近1.5MB,页面资源的大部分是javascript和图像。站点体积日趋变大和网络延迟,CPU限制,渲染阻塞,冗余的第三方代码,为复杂的性能优化难题贡献了很多力量。

绝大多数用户把网站加载速度作为用户体验金字塔(UX hierarchy)中最重要的部分。这并没有让人感觉到意外,我们在等待网页完成加载过程中,基本上不会去做其他事请,但是这个过程中我们无法获取到页面的值,我们无法欣赏网站的设计美学。

用户体验金字塔

图一 速度对用户来说很重要(Speed Matters, Vol .3)

网站性能对用户体验很重要,但是寻找优化的突破口就像寻找宝藏一样。幸运的是,现在有些工具可以帮助我们。

Lighthouse – 性能工作流标配工具

Lighthouse是Chrome开发者工具中的一部分,这个工具可以给我们的网站做一个性能审计,并提供让站点更好的意见。

我们最近在这个工具上推出了一系列新的性能审核,这些审核在日常开发工作流程中非常有用。

灯塔审计的几个点

新的灯塔审计

现在我们举一个实际的例子: The Oodles Theater app. 怎么使用Lighthouse来优化这个demo网站,在这个网站中大家可以试一试互动版的google涂鸦,甚至玩一两局游戏。

在构建网站的过程中,我们都想要站点越快越好,尽可能高效,性能优化的起点就是利用Lighthouse报告。

Oodles app 的灯塔报告

Oodles app 的灯塔报告

Oodles的Lighthouse性能报告是相当的糟糕。在一个3G网络中,用户需要等待15秒的时间加载出第一个有内容的界面或者app能够交互。LightHouse指出了一大堆需要优化的点,性能指标得23分也认证这一点。

这个页面有3.4MB!em,em 我们需要给它减肥。

现在我们开始性能优化的第一个挑战,找到那些能够容易删除但并不影响用户体验的东西。

性能优化的点

删除不必要的资源

从性能报告中,很明显有些东西能够被删除点,代码中的空格和分号。

最小化并压缩Javascript 和CSS

最小化并压缩Javascript 和CSS

Lighthouse在Unminified CSS & JavasScript audit 指出了这个点。在打包过程中我们可以使用webpack的Uglify JS plugin 来达到这个目的。

最小化脚本和css是一个司空见惯的优化点,我们很容易找到解决方案。另一个有用的方案是启用文本压缩(gzip压缩)我们没有必要发送不压缩的文件。大部分CDN都默认支持gzip压缩。

我们利用FrieBase Hosting 部署我们的代码。Firebase默认支持gzipping 。所以我们可以免费获得在CDN托管代码的优点。

虽然gzip是个使用非常广泛的压缩方式,像ZopfliBrotli 这两种压缩方式关注度也越来越高。现在大多数浏览器都支持Brotli,我们可以把压缩好的二进制文件发送到服务器上去,(注:现在IE11–[包括ie11]以下的浏览器不支持Brotli压缩算法,Brotli比gzip压缩比更高,更加节省体积,nginx 有Brotli模块,目前国内cdn服务商,又拍云支持brotli模块,其他的云服务不太清楚,阿里云cdn不支持,nginx启用Brotli配置见链接

浏览器Brotli算法支持列表

Brotli算法支持列表

使用有用的缓存策略

我们接下来的目标是如果没有必要,我们确保相同资源不要发送两次。

Lighthosue无效的缓存策略审计可以帮助我们优化缓存来达到上述目的。在服务中设置为响应头中的Cache-Control设置为max-age=xxx秒,来实现上述目的(注:http协议的缓存策略可以查看Http缓存)。这样我们确保在重复访问的过程中能够再次利用缓存好的资源。

理想情况下,我们应该尽可能多的缓存我们的资源,并且缓存时间越长越好,并且在资源更新之后提供校验。(注:现在发布web服务,采用增量发布,而不是覆盖发布,每个文件把自己的指纹[hash值]添加到文件名中,通过文件名的变化[url发生变量]来更新资源,这个避免缓存不同步的问题),

删除无用资源

现在我们解决不必要的下载这个优化点,那现在还是剩下哪些很明显可以优化点呢?举个例子,无用的代码

检查代码覆盖率

检查代码覆盖率

有时候,我们在我们的apps包含了那些不必要的代码,特别是一个app维护了很长时间,我们的团队或我们的依赖发生变。在app迭代的过程中,我们忘记了删除了无用的库,这样的事情一直发生。

开始我们使用Material组件库快速开发我们的app,在迭代的过程中,为了让app用户体验更好,我们使用自己设计UI,但是我们忘记删除Meterial组件库,幸运的是,Code coverage检查帮助我们发现无用的Meterial组件库。

在app运行或app加载完之后 都可以通过DevTools中检查代码使用率。在Coveage栏中我们可以看到两条红色的条纹。我们有超过95%的css是无用的,此外也有很多Javascript是无用的。

LightHouse 在无用的CSS rules审计中有指出了这个点,它指出大约可以节省超过400Kb的CSS。所以,我们现在检查我们的代码,删除无用的javascript 和css样式。

如果我们删除MVC适配器,我们的样式将减少到10KB!

如果我们删除MVC适配器,我们的样式将减少到10KB!

这使我们的css文件大小减少了20倍,这个非常有用。当然,在Lighthouse中,性能指数分散也上升了,页面能够交互所需时间也变得更好。

但是,虽然我们做了以上的优化,但是我们仅仅关注LightHouse的指标是不够的,删除之前的无用的代码并不是没有风险的,我们有有必要进行回归测试。

我们的样式有95%是无用的,那么仍然有5%代码在其他地方用到。很明显其中的一个组件仍然在使用样式库的样式-涂鸦轮播图下的小箭头在使用。我们可以手动的把该样式加入到button中。

button 组件仍然在使用移除的样式库

button 组件仍然在使用移除的样式库

如果我们删除了代码,我们确保我们有恰当的测试流程,来帮助我们预防潜在的视觉回归(注:进行视觉回归测试

避免巨大的网络负载

我们很清楚大量的资源能够降低我们网页的加载速度。流量消耗过多影响用户的钱袋子,而且用户对流量消耗过多有很大的抵触,所以这个优化很重要。

Lighthouse也有能力检测出我们加载的网络资源过多

检测出巨大的网络资源加载

检测出巨大的网络负荷

我们可以看到有超过3M的代码被加载,这个太大了,特别是在手机上。在图片最上面的列表中,我们可以看到vendor.js不压缩的体积超过了2MB。这个在webpack打包中也被高亮提醒过大。

俗话说:最快的请求就是不发送任何请求。

理想情况下,我们应该衡量为用户服务的每一项资源的价值,衡量这些资源的表现,并根据初始经验来判断是否值得发送给用户。因为有时这些资源可以在空闲时间延迟,延迟加载或处理。

在这的例子中,因为我们正在处理大量的JavaScript包,但是我们很幸运,因为现在JavaScript社区拥有丰富的JavaScript包检查工具。

检查javascript bundle

检查javascript bundle

我们通过 webpack bundle analyzerl开始我们的分析工作。webpack bundle analyzer告诉我们依赖项 unicode 的体积有1.6MB ,这是相当大的一个包。

此外,我们也可以通过vs code 中的Import Cost Plugin for Visual code插件分析每个包的大小.通过这个我们很容易发现哪个组件引用了这个库。

我们也可以使用另外一个工具,BundlePhobia ,我们输入npm包的名字,就能够预估包最小化和gzip之后的体积。为了解决unicode包过大的问题,我们找到替换uicode包的替换品slug模块,该模块只有2.2Kb,果断替换。

这个改变让性能提高很多,我们也尽可能寻找机会减少javascript包的体积,我们总共减少了2.1Mb的代码体积。

通过减少代码体积并进行gizp压缩之后,性能指数提高了65%,这是一个很值得做的事情。因此,我们需要尝试消除网站和应用中不必要的资源。我们对资源进行清点并计算它带来的效益(很值得去做,用户体验提高,网站访问量也会上升,这个有相关数据证明),所以定期去检查是否使用到无用的代码。

通过代码分割降低javascript启动时间

虽然过多网络有效负载可能对我们的应用程序产生重大影响,但还有另一件事也会产生巨大影响,那就是JavaScript。

Javascript是网页中最昂贵的资源,如果我们一个页面加载过多的javascript。这个能够大大延迟用户体验网站的时间,也就是会发生他们点击任何UI却无任何反应的情况。所以我们需要明白执行JavaScript的代价很高非常重要。

下面是浏览器处理javascript脚本的过程

浏览器执行javascript的过程

浏览器执行javascript的过程

首先浏览器得下载JavaScript脚本,然后有一个javascript引擎去解析JavaScript脚本,之后进行编译,最后去执行该脚本。这些过程在高性能的设备,像台式电脑,或笔记本,或一台高性能的手机上,但是在一个中配的手机上,可能需要5到10倍的时间,这也是响应变慢的原因,所以裁剪JavaScript很重要。

为了清楚app中处理javascript需要的时间,Lighthouse给我们提供了JavaScript boot-up audit

javacript脚本启动的检查报告图

javacript脚本启动的检查报告

在Oodle app这个例子中,Lighthouse告诉我们javascript启动需要花费1.8s的时间。原因是我们把所有的路由和组件静态的引入到一个JavaScript文件中。解决这个问题一项技术是代码分割。

代码分割是不一次性给用户一整块JavaScript 披萨,而是某个时间段他们需要哪块给哪块。代码分割可以应用在路由级层面或组件级层面。React ,Reat Loadable ,Vue, Angular, Polymer, Preact以及绝大多数框架都支持代码分割。

接下来,我们把代码分割加入我们的app代码中,用动态导入(dynamic import)代替静态导入(static import),这样可以实现异步懒加载代码,只有用户使用需要的代码时,加载对应的代码(注:通过类似jsonp的形式加载对应脚本,但是让脚本在正确的上下文执行环境)。

利用dynamic import 进行代码分割示意图

利用dynamic import 进行代码分割

这个操作不但减少了包体积,也降低了JavaScript启动时间。代码分割让JavaScript启动时间下降到0.78s。使app启动快了56%。一般而言如果我们开发包含很多javascript的app,我们应该发送用户需要的那些代码。

在利用webpack的过程中,我们要有效的利用代码分割,webpack的tree shaking功能,以及webpack-libs-optimizations 报告的信息,尽量来减少代码的体积。

优化图片

在Oodle应用程序中,我们使用了大量图像。 不幸的是,灯塔对图片的热情远远低于我们。 事实上,我们在所有三个与图像相关的审核上都失败了。 我们忘记优化我们的图像,我们没有正确地调整它们,我们也可以通过使用其他图像格式获得一些收益。

现在我们开始优化图片。我可以使用ImageOptim 或者XNConvert这些可视化工具优化我们的图片(注:其实tinyPng网站的图片优化能力更强,图片压缩比更高,而尽量减少图片视觉上的损失)。更加自动化的手段是把图片优化步骤添加到打包过程中,如利用imagemin库(注:这个自动化手段,还是要看打包的机器性能,优化图片是密集型计算,这个会影响到打包的时间,导致打包时间过程过长,这个因项目而异,我不提倡将优化图片过程合并到打包中,而是单独提出来

这个可以确保图片优化变得自动化,其实现在一些CDN服务,如Akamai或第三方解决方案如CloudinaryFastly提供了全方位的图片优化方案,我们可以把这些图片存储到这些服务上(注:现在国内存储服务商都提供了对应的解决方案,有的还支持转成webp图片格式,云服务让图片处理更加方便,快捷

如果因为钱的或延迟的问题,我们不想使用这些图像处理服务,我们可以通过Thumbor或者Imageflow自己搭建图片处理服务。

图片优化前后的对比

图片优化前后的对比

背景图片在优化前被高亮标记,并显示图片过大,通过ImageOptim压缩之后,图片减少到100Kb,这个是可以接受的。对站点上的大部分图像进行优化,这个能够减轻我们网站的体积。

使用正确的方式显示动画内容

利用GIF可能会变得非常昂贵。让人惊讶的是,GIF格式从未打算成为动画平台,因此,切换到更合适的视频格式可以大大节省文件大小。

使用视频替换gif格式的动画

使用视频替换gif格式的动画

我们可以使用FFmpeg来将gif格式的动画转换成mp4格式的文件。WebM格式的视频能够更节省空间,ImageOptim提供了对应的转换接口。

ffmpeg -i animation.gif -b:v 0 -crf 40 -vf scale=600:-1 video.mp4

通过转换格式,大约节省了80%的体积,我们将文件大少减少到1Mb,但是1Mb的大小文件,对于网络环境很差的用户来说,还是太大了。这个时候我们可以通过Effective Type API来判断用户处于的网路环境,如果网络环境较差,我们用图片来JPEG图片来替换。

该接口使用有效的往返时间和downing values 来预估用户正在使用的网络类型。它只返回一个字符串,slow 2G, 2G, 3G or 4G.。因此,我们根据该字符串来判断如果用户低于4G,我们可以用图像替换视频元素。

if (navigator.connection.effectiveType) { ... }

这个会降低用户体验,但是我们能够保证在较差网络环境中,站点可用。

懒加载屏幕以外的图片

转盘,轮播图或非常长的页面通常会加载图像,即使用户无法立即在页面上看到它们。Lighthouse将会在off-screen image audit高亮在屏幕之外未懒加载的图片,我们也可以在Dev tools的Network栏中看到这些图片。如果一个页面有很多图,但是页面上只有少数是可以看见的,那么我们要考虑使用图片懒加载了。

浏览器并不支持支持懒加载,我们必须使用javascript实现这个功能。在Oodle封页中,我们利用Lazysizes library实现懒加载。

<!-- Import library -->
import lazysizes from 'lazysizes'  <!-- or -->
<script src="lazysizes.min.js"></script>

<!-- Use it -->

<img data-src="image.jpg" class="lazyload"/>
<img class="lazyload"
  data-sizes="auto"
  data-src="image2.jpg"
  data-srcset="image1.jpg 300w,
    image2.jpg 600w,
    image3.jpg 900w"/>

Lazysizes相当智能,因为它不仅可以跟踪元素的可见性变化,还可以主动预取视图附近的元素,以获得最佳的用户体验。 它还提供了IntersectionObserver的可选集成,为您提供非常高效的可见性查找。 添加懒加载后,我们的图像将按需提取。 如果您想深入了解该主题,请查看images.guide – 一个非常方便和全面的资源。

帮助浏览器尽早提供关键资源

并非是所有通过网络发送到浏览器的字节都具有相同程度的重要性,浏览器也很清楚这一点。许多浏览器都清楚他们首先应该获取什么资源,所以有些时候浏览器会在加载加载脚本和图片之前去加载css样式。

有些有用的东西,我们这些开发者更加清楚,我们可以告诉浏览器对于我们来说真正重要的是什么。让人感到高兴的是,在过去几年,浏览器供应商添加许多功能来解决这个问题。比如,资源提示link rel =preconnect 和 preload 以及 prefetch

这些新增的能力可以帮助浏览器在正确的时间获取正确的东西,并且他们比使用脚本完成的一些自定义加载,基于逻辑的方法更加有效。

那么让我们来看看Lighthouse是如果指导我们有效地使用这些功能的。

避免多次的昂贵的往返延迟

避免多次的昂贵的往返延迟

在Oodle App这个例子中,我们到处都用到了Google字体。无论页面什么时候使用了goole字体样式表,我们都需要与两个子域名建立连接。Lighthouse 告诉我们如果我们为该连接热身(注:其实在发送http之前进行了DNS解析,TLS协商(如果是https),建立TCP连接)那么我们能够节省3ooms的连接时间。

利用 link rel preconnet 的优点,我们能够有效避免连接延迟。

特别是谷歌字体face css 托管在googleapis.com,字体资源托管在Gstatic,这个对象性能影响很大,利用preconnect来优化连接,我们可以节省几百毫秒的时间。

Lighthouse接下来的建议是预加载关键请求。

预加载关键资源提示图

预加载关键资源

<link rel=preload>相当的强大,它会通知浏览器该资源是当前页面需要的资源,它会尝试让浏览器尽快获取它(优先级高)。

现在Lighthouse提醒我们我们应该预加载关键的web字体资源,因为我们正在加载两个两个字体。

预加载web字体像这样定义,rel=preload as=”font” 然后指定我们要传递的字体类型,例如woff2。

这个会对页面产生非常大的影响

预加载资源与没有预加载资源对比图

预加载资源的影响

一般来说,如果webfont是网页中很重要的内容,但我们又没有使用link rel preload。这个时候浏览器第一个做的事情是获取HTML页面,解析css,之后会在某个时候获取我们的web字体。

如果我们使用了link rel preload ,那么我们在获取HTML页面之后,浏览器一解析html页面,就能去获取web font字体。在Oodle App这个例子中,可以减少我们使用我们的Web字体渲染文本所花费的时间。

如果我们使用google字体,由于我们的团队在不断更新web font 样式,这经常会导致url发生变化,或者不可用,我建议自己托管自己的字体,这样我们就能够充分利用preload的功能。

在Oodle App, 我们利用Google Web Fonts Helper来下载字体,部署到自己的服务上,这个工具特别棒,你们可以试一试。

不管怎么,如果我们页面中有很重要的资源,不管是字体 还是javascript,那么使用preload帮助很大。

实验功能:优先级提示

今天我们有特别的东西与你分享。除了资源提示和预加载等功能外,我们还在开发一种全新的实验性浏览器功能,我们称之为优先级提示。

优先级提示的代码编写图

优先级提示

这个一项新功能,这个可以告诉浏览器资源重要性程度,通过 importance 来设置importance 有三个可选值分别为 high low auto。

这个特性,可以让我们降低非关键资源的优先级,如非关键样式表,图片,或 调用的fetch API,来减少资源的争抢,并提高关键资源的优先级,如Oodle App中的hero Images图片。

Oodle App这也是一个可以优化的选项。

设置初始化可见内容的优先级

设置初始化可见内容的优先级

在对图片启用懒加载之前,浏览器以非常高的优先级一次性加载了轮播图中的所有涂鸦图片。很不幸的是,视图中间的图片对于用户来说才是最重要的,于是我们将背景图优先级设置的非常低,而将前景图片的优先级设置的非常高,在慢速3G模式下,用户能够提前2s看到图片内容。通过设置优先级提示,我们能够更快的获取并渲染的图片,这是个很棒的体验。

这个特性,在Chrome Canary这个版本添加,大家拭目以待吧。

Web字体加载策略

排版是良好设计的基础,如果我们使用的是Web字体,理想情况下我们不希望阻止文本的渲染,而且绝对不希望显示不可见的文本。

Lighthouse会通过avoid invisible text while web fonts are loading 这一项审计报告,来强调这一点。

 

避免加载不可见文本的字体

避免加载不可见文本的字体

如果我们使用font-face来加载字体,这个会让浏览器去决定去能够忍受花多长时间等待字体的下载,有些浏览器等待三秒之后,如果没有加载完字体,就会使用系统字体来渲染文本,等字体加载完之后,再替换系统字体(注:如果字体三秒内没有下载完,等待三秒,然后再使用系统字体替换,,而不是提前渲染文本,这个时间太长,用户体验很不好

在可见文本中,我们应该尽力避免这种情况,如果加载字体时间过长,那么我们无法在短时间内看到经典的涂鸦文本。但是,很高兴的是,我们可以通过font-display这个属性,来更好的控制这个过程(注:chrome浏览器60+版本支持这个选项

@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: local('Montserrat Regular'), local('Montserrat-Regular'),
      /* Chrome 26+, Opera 23+, Firefox 39+ */
      url('montserrat-v12-latin-regular.woff2') format('woff2'),
        /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
      url('montserrat-v12-latin-regular.woff') format('woff');
}

font-display可帮助根据交换所需的时间来决定Web字体的呈现方式或后备方式(注:font-display的详细介绍

在Oolde App中 对font-display 我们使用swap选项。swap选项为字体提供了0秒等待周期和无限长的交换周期。意思是浏览器一开始渲染文本使用备用字体,一直等待字体库加载完之后,使用加载完的字体库替换备用字体库渲染文本注:在自己部署字体库的时候,我们需要设置字体的缓存时间,尽量的长,而不是每次加载字体库,对于woff,svg等字体库进行压gzip压缩,降低下载时长)。

Oolde App 使用这个属性之后,我们能够更快的看到有意义的文本,等web字体加载完之后,立即替换系统字体。

字体显示结果

如果我们使用了web font,而且占比很大,那么我们需要一个好的字体加载策略。有许多博客提到字体加载优化,我推荐一个 Zach Leatherman’s的Web Font Recipes repo ,他写的太好了。

减少渲染阻塞脚本

应用中的一些东西,我们应该更早的推送,以便给用户更早的提供一些基本体验。在Lighthouse的加载时间线中,我们可以看到在所有资源加载前的一段时间,用户基本上看不到任何东西。

减少阻塞渲染的样式的优化点

减少阻塞渲染的样式的优化点

下载外部样式阻塞了我们的渲染进程,我们可以尝试通过稍早提供一些样式来优化我们的关键渲染路径,如果我们把初始的渲染样式内联到html文本中,那么浏览器能够更快的渲染样式,而不必要等待外部样式加载完成再去渲染页面(注:现在基本上是把index.html部署静态服务器上,而样式表部署到cdn服务上,现在开发基本上提取所有的样式到一个文件中,这种做法要我们样式表的大小,如果过大,其实可以基于路由或组件分拆样式,而不是把所有样式提取到一个文件中,如果首页样式不大,那么可以把首页样式内联到html文件中,来减少一次请求达到优化速度)。

我们可以通过Critical这个NPM将我们关键的样式在打包过程中内联到html文件中,这个需要我们做好架构设计,在设计架构之前,我们需要考虑性能的问题。

优化之后的结果

经过一系列之后的优化之后,我们看一下优化的结果。下面的视频是在3G网络下,中配的手机上优化前后的对比视频

Lighthouse性能分数从23飙到了91分,这是一个相当好的进展。所有的这些改变都是我们遵守Lighthouse报告得到的结果。如果您想了解我们如何在技术上实施所有改进,请随时查看我们的github,尤其是那些的PR(pull request)

预测性能-数据驱动用户体验

我们相信机器学习在许多领域为未来提供了令人兴奋的机会。我们希望将来会引发更多实验的一个想法是,利用真实数据真正指导我们的用户体验。 今天,我们对用户可能想要或需要的内容都是自己的个人观点,,然后对这些资源进行prefetch preload或 pre-cache,如果我们猜对了。

我们实际上有数据可以更好地为我们今天的优化提供决策。利用Google Analytics reporting API,我们可以查看站点的留存比,各个url的退出比,利用这些信息来决定优化哪些资源。如果我们将其与良好的概率模型相结合,我们可以积极的提前获取用户需要的内容,避免浪费用户流量。我们利用Google Analytics数据,并使用像马尔可夫链神经网络等深度学习模型来达到目的(注:数据驱动扭曲立场

数据驱动来改善改善web体验

为了推进这个过程,我们真正实行一个叫gusss.js的计划。

guess-js

Guess.js是一个专注于网络数据驱动用户体验的项目。我们希望它能激发人们探索使用数据来改善网络性能,它在GitHub上开源的项目。这个项目由Minko Gechev,Gatsby的Kyle Matthews,Katie Hempenius和其他一些人与开源社区合作建立的。

总结

Lighthouse上分数和指标帮助我们提高Web的速度,但它们只是手段,而不是目标本身。 我们都体验过慢速页面加载,但我们现在有机会为用户提供更加愉快的加载体验。 提高网页性能做不完的,许多小的变化可以带来巨大的收益。通过使用正确的优化工具并密切关注Lighthouse报告,我们可以为用户提供更好,更具包容性的体验。 在此特别感谢:Ward Peeters,Minko Gechev,Kyle Mathews,Katie Hempenius,Dom Farolino,Yoav Weiss,Susie Lu,Yusuke Utsunomiya,Tom Ankers,Lighthouse和Google Doodles。

 

 

发表评论