资源请求与缓存

资源请求与缓存

数据压缩与流式加载

HTTP 缓存

CDN

HTTP/2 服务端推送

使用 HTTPS 安全传输

Font | 字体

Custom Web Fonts

我们首先回顾下浏览器是如何使用自定义字体的,当浏览器识别到用户在 CSS 中基于@font-size定义的字体时,会尝试下载该字体文件。而在下载的过程中,浏览器是不会展示该字体所属的文本内容,最终导致了所谓的Flash of Invisible Text现象。现在很多的网站都存在这个问题,这也是导致用户体验差的一个重要原因,即会影响用户最主要的内容浏览这一操作。而我们的优化点即在于首先将字体设置为默认字体,而后在自定义的 Web Font 下载完毕之后对标准字体再进行替换操作,并且重新渲染整个文本块。而如果自定义的字体下载失败,整个内容还是能保证基本的可读性,不会对用户体验造成毁灭性的打击。

首先,我们会为需要使用到的 Web Fonts 创建最小子集,即只将那些需要使用的字体提取出来,而并不需要让用户下载整个字体集,这里推荐使用Font squirrel webfont generator。另外,我们还需要为字体的下载设置监视器,即保证能够在字体下载完毕之后自动回调,这里我们使用的是fontfaceobserver,它会为页面自动创建一个监视器,在侦测到所有的自定义 Web Fonts 下载完毕后,会为整个页面添加默认的类名:

html {font-family: Georgia, serif;}
html.fonts-loaded {font-family: Noto, Georgia, serif;}

不过现在 CSS 的font-display属性也原生提供了我们这种替换功能,更多详情可见font-display属性。

服务端与缓存

高性能的前端离不开服务端的支持,在我们的实践中也发现不同的服务端配置同样会影响到前端的性能。目前我们主要使用 Apache Web Server 作为中间件,并且通过 HTTPS 来安全地传递内容。

Configuration

我们首先对于合适的服务端配置做了些调研,这里推荐是使用H5BP Boilerplate Apache Configuration作为配置模板,它是个不错的兼顾了性能与安全性的配置建议。同样地它也提供了面向其他服务端环境的配置。我们对于大部分的 HTML、CSS 以及 JavaScript 都开启了 GZip 压缩选项,并且对于大部分的资源都设置了缓存策略,详见下文的 File Level Caching 章节。

HTTPS

使用 HTTPS 可以保证站点的安全性,但是也会影响到你网站的性能表现,性能损耗主要发生在建立 SSL 握手协议的时候,这会导致很多的延迟,不过我们同样可以通过某些设置来进行优化。

  • 设置 HTTP Strict Transport Security 请求头可以让服务端告诉浏览器其只允许通过 HTTPS 进行交互,这就避免了浏览器从 HTTP 再重定向到 HTTPS 的时间消耗。
  • 设置 TLS false start 允许客户端在第一轮 TLS 中就能够立刻传递加密数据。握手协议余下的操作,譬如确认没有人进行中间人监听可以同步进行,这一点也能节约部分时间。
  • 设置 TLS Session Resumption,当浏览器与服务端曾经通过 TLS 进行过通信,那么浏览器会自动记录下 Session Identifier,当下次需要重新建立连接的时候,其可以复用该 Identifier,从而解决了一轮的时间。

这里推荐扩展阅读下Mythbusting HTTPS: Squashing security’s urban legends by Emily Stark

Cookies

我们并没有使用某个服务端框架,而是直接使用了静态的 Apache Web Server,不过 Apache Web Server 也是能够读取 Cookie 并且进行些简单的操作。譬如在下面这个例子中我们将 CSS 缓存信息存放在了 Cookie 中,然后交付 Apache 进行判断是否需要重复加载 CSS 文件:

<!-- #if expr="($HTTP_COOKIE!=/css-loaded/) || ($HTTP_COOKIE=/.*css-loaded=([^;]+);?.*/ && ${1} != '0d82f.css' )"-->

<noscript><link rel="stylesheet" href="0d82f.css"></noscript>
<script>
(function() {
  function loadCSS(url) {...}
  function onloadCSS(stylesheet, callback) {...}
  function setCookie(name, value, expInDays) {...}

  var stylesheet = loadCSS('0d82f.css');
  onloadCSS(stylesheet, function() {
    setCookie('css-loaded', '0d82f', 100);
  });
}());
</script>

<style>/* Critical CSS here */</style>

<!-- #else -->
<link rel="stylesheet" href="0d82f.css">
<!-- #endif -->

这里 Apache Server 中的逻辑控制代码就是有点类似于注释形式的<!-- #,其主要包含以下步骤:

  • $HTTP_COOKIE!=/css-loaded/ 检测是否有设置过 CSS 缓存相关的 Cookie
  • $HTTP_COOKIE=/.*css-loaded=([^;]+);?.*/ && ${1} != '0d82f.css'检测缓存的 CSS 版本是否为当前版本
  • If <!-- #if expr="..." --> 值为true 我们便能假设该用户是第一次访问该站点
  • 如果用户是首次浏览,我们添加了一个<noscript>标签,里面还包含了一个阻塞型的<link rel="stylesheet">标签。添加该标签的意义在于我们在下面是使用 JavaScript 来异步加载 CSS 文件,而在用户禁止 JavaScript 的情况下也能保证可以通过该标签来正常加载 CSS 文件。
  • <!-- #else --> 表达式在用户二次访问该页面时,我们可以认为 CSS 文件已经被加载过了,因此可以直接从本地缓存中加载而不需要重复请求。

上述策略同样可以应用于 Web Fonts 的加载,最终的 Cookie 如下所示:

File Level Caching

在上文可以发现,我们严重依赖于浏览器缓存来处理用户重复访问时资源加载的问题,理想情况下我们肯定希望能够永久地缓存 CSS、JS、Fonts 以及图片文件,然后在某个文件发生变化的时候将缓存设置为失效。这里我们设置了以https://www.voorhoede.nl/assets/css/main.css?v=1.0.4形式,即在请求路径上加上版本号的方式进行缓存。不过这种方式的缺陷在于如果我们更换了资源文件的存放地址,那么所有的缓存也就自然失效了。这里我们使用了gulp-rev以及gulp-rev-replace来为文件添加 Hash 值,从而保证了仅当文件内容发生变化的时候文件请求路径才会发生改变,即将每个文件的缓存验证独立开来。

Result

上面我们介绍了很多的优化手段,这里我们以实验的形式来对优化的结果与效果进行分析。我们可以用类似于PageSpeed Insights或者WebPagetest来进行性能测试或者网络分析。我觉得最好的测试你站点渲染性能的方式就是在限流的情况下观察页面的呈现效果,Google Chrome 内置了限流的功能:

这里我们将我们的网络环境设置为了 50KB/S 的 GPRS 网络环境,我们总共花费了 2.27 秒完成了首屏渲染。上图中黄线左侧的时间即指明了从 HTML 文件开始下载到下载完成所耗费的时间,该 HTML 文件中已经包含了关键的 CSS 代码,因此整个页面已经保证了基本的可用性与可交互型。而剩下的比较大的资源都会进行延时加载,这正是我们想要达到的目标。我们也可以使用 PageSpeed 来测试下网站的性能,可以看出我们得分很不错:
而在 WebPagetest 中,我们看出了如下的结果:

Roadmap

优化之路漫漫,永无止境,我们在未来也会关注以下几个方面:

  • HTTP/2:我们目前已经开始尝试使用 HTTP/2,而本篇文章中提到的很多的优化的要点都是面向 HTTP/1.1 的。简言之,HTTP/1.1 诞生之初还是处于 Table 布局与行内样式流行的时代,它并没有考虑到现在所面对的 2.6MB 大小,包含 200 多个网络请求的页面。为了弥合这老的协议的缺陷,我们不得不连接 JS 与 CSS 文件、使用行内样式、对于小图片使用 Data URL 等等。这些操作都是为了节约请求次数,而 HTTP/2 中允许在同一个 TCP 请求中进行多个并发的请求,这样就会允许我们不需要再去进行大量的文件合并操作。
  • Service Workers:这是现代浏览器提供的后台工作线程,可以允许我们为网站添加譬如离线支持、推送消息、后台同步等等很多复杂的操作。
  • CDN:目前我们是自己维护网站,而在真实的应用场景下可以考虑使用 CDN 服务来减少服务端与客户端之间的物理距离,从而减少传输时延。
  1. 最多三秒钟渲染完成单屏或者使用 Loading
  2. 基于 3G/4G 移动网络下,每屏幕资源不超过 1024KB
加载优化
  • 合并 CSS、JavaScript
  • 合并小图片、使用雪碧图
  • 缓存一切可以缓存的资源,部分资源 css、js 使用src="abc.css?cacheVersion=1"来控制版本
  • 使用长 Cache
  • 压缩 HTML、CSS、JS
  • 启用GZip
  • 使用首屏加载
  • 使用按需加载
  • 使用滚屏加载
  • 增加进度指示器
  • 减少Cookie
  • 避免重定向
  • 异步加载第三方资源
CSS 优化
  • CSS 写在头部,JS 写到尾部或者异步
  • 避免图片和iFrame等的 SRC 为空
  • 尽量避免重设图片大小
  • 图片尽量避免使用DataURL
  • 尽量避免在 HTML 标签中写 Style
  • 避免 CSS 表达式
  • 移除空的 CSS 规则
  • 正确使用 Display 的属性
  • 不滥用Float
  • 不滥用 Web 字体
  • 不声明过多的Font-size
  • 值为 0 时候不需要任何单位
  • 标准化各种浏览器的前缀
  • 避免让选择符看起来像正则表达式
图片优化
  • 使用 CSS3、SVG、IconFont代替图片
  • 使用 Srcset
  • WebP 优于 JPG
  • PNG8 优于 GIF
  • 首次加载不大于 1024KB 单页
  • 图片不宽于 640
脚本优化
  • 减少重绘
  • 缓存 Dom 选择与计算
  • 缓存列表的长度
  • 尽量使用事件代理,避免批量绑定事件
  • 尽量使用 ID 选择器
  • 使用 touch 代理 click
渲染优化
  • HTML 使用 ViewPort
  • 减少 Dom 节点
  • 尽量使用 CSS3 动画
  • 合理使用requestAnimationFrame动画代替 setTimeout
  • 适当使用 Canvas 动画
  • touchmove,scroll事件会导致多次渲染
  • 使用 CSS3-transitions、CSS3-3D、Opacity、Canvas、WebGL、Video来触发 GPU 渲染

本文从属于笔者的Web 前端入门与最佳实践前端性能优化系列,同时也归纳于笔者的我的校招准备之路:从 Web 前端到服务端应用架构这篇综述。

前端优化的根本目的是为了有一个更好地用户体验的同时尽可能减少后端负载压力。即保证更少的加载时间、更快的首屏渲染、更流畅的用户交互。在笔者自己的知识体系内,当我们想为用户呈现更好的视觉效果与用户体验时,我们往往会从性能评测与监控资源与请求优化加载策略首页与关键路径渲染优化这几个方面进行考虑。

编码与压缩

Image Optimization:图片使用与显示优化

WebP

HTTP Cache

静态网站非常简单,它就是通过一个 url 访问 web 服务器上的一个网页,web 服务器接收到请求后在网络上使用 http 协议将网页返回给浏览器,浏览器通过解析 http 协议 最终将页面展示在浏览器里,有时这个网页会比较复杂点,里面包含了一些额外的资源例如:图片、外部的 css 文件、外部的 js 文件以及一些 flash 之类的 多媒体资源,这些资源会单独使用 http 协议把信息返回给浏览器,浏览器从页面里的 src,href、Object 这样的标签将这些资源和页面组合在一 起,最终在浏览器里展示页面。但是不管什么类型的资源,这些资源如果我们不是手动的改变它们,那么我们每次请求获得结果都是一样的。这就说明静态网页的一 个特点:静态网页的资源基本是不会发生变化的。因此我们第一次访问一个静态网页和我们以后访问这个静态网页都是一个重 复的请求,这种网站加载的速度基本都是由网络传输的速度,以及每个资源请求的大小所决定,既然访问的资源基本不会发生变化,那么我们重复请求这些资源,自 己在那里空等不是很浪费时间吗?如是乎,浏览器出现了缓存技术,我们开发时候可以对那些不变的资源在 http 协议上编写相应指令,这些指令会让浏览器第一 次访问到静态资源后缓存起这些静态资源,用户第二次访问这个网页时候就不再需要重复请求了,因为请求资源本地缓存,那么获取它的效率就变得异常高效。

CDN

多域名资源存放

  1. 静态内容和动态内容分服务器存放,使用不同的服务器处理请求。处理动态内容的只处理动态内容,不处理别的,提高效率,这样使得 CDN(内容分发网络)缓存更方便

2、突破浏览器并发限制 (你随便挑一个 G 家的 url: https://lh4.googleusercontent.com/- si4dh2myPWk/T81YkSi__AI/AAAAAAAAQ5o/LlwbBRpp58Q/w497-h373/IMG_20120603_163233.jpg, 把前面的 lh4 换成 lh3,lh6 啥的,都照样能够访问,像地图之类的需要大量并发下载图片的站点,这个非常重要。)

3、跨域不会传 cookie,节省宽带;举例说一下: twitter 的主站 http://twitter.com,用户的每次访问,都会带上自己的 cookie,挺大的。假如 twitter 的图片放在主站域名下,那么用户每次访问图片时,request header 里就会带有自己的 cookie,header 里的 cookie 还不能压缩,而图片是不需要知道用户的 cookie 的,所以这部分带宽就白白浪费了。 写主站程序时,set-cookie 也不要 set 到图片的域名上。 在小流量的网站,这个 cookie 其实节省不了多少带宽,当流量如 facebook twitter 时,节省下来就很可观了。

关于多域名,也不是越多越好,虽然服务器端可以做泛解释,浏览器做 dns 解释也是耗时间的,而且太多域名,如果要走 https 的话,还有要多买证书和部署的问题

Static Content:网站静态化

动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后,我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路。由此可见,网站静态化处理的核心就是动静分离和缓存两大方面,上篇我简单讲述了动静整合的基础知识,本篇将会讲述两大核心之一的动静分离策略,只有把动静分离策略做好了,缓存才能发挥出它应有的效果。

请求优化

Throttling: 限流

function ajaxThrottle(url, params, timeout) {
  // 这里的缓存暂时挂载在 window 命名空间下
  if (!window.ajaxThrottleContext) {
    window.ajaxThrottleContext = {};
  }

  let token = url + JSON.stringify(params);

  let lastAjaxTime = window.ajaxThrottleContext[token];

  // 判断是否已经过时
  if (lastAjaxTime && Date.now() - lastAjaxTime < timeout) {
    // console.log(`Request Abort: ${token}`);
    return false;
  }

  window.ajaxThrottleContext[token] = Date.now();
  return true;
}

jquery.ajaxDebounce

资源预抓取

譬如 instant.page 能够利用即时预加载 - 它在用户点击之前预先加载页面。在用户点击链接之前,他们将鼠标悬停在该链接上;大概在 65 毫秒时,他们可能才会实际点击该链接,instant.page 此时开始预加载,平均超过 300 毫秒,以便页面预加载。在移动设备上,用户在释放之前开始触摸他们的显示器,平均花费 90 毫秒来预加载页面。

const prefetcher = document.createElement("link");

// ...

function preload(url) {
  prefetcher.href = url;
}

function touchstartListener(event) {
  const linkElement = event.target.closest("a");

  // ...
  linkElement.addEventListener("touchcancel", touchendAndTouchcancelListener, {
    passive: true,
  });
  linkElement.addEventListener("touchend", touchendAndTouchcancelListener, {
    passive: true,
  });

  urlToPreload = linkElement.href;
  preload(linkElement.href);
}

Links

上一页