从制作世界上最快的网站中学到的十件事 [译] - lotuc - 知乎专栏

1,027 阅读8分钟
原文链接: zhuanlan.zhihu.com

本文译自 10 things I learned making the fastest site in the world

本文是聊的是性能提升相关的技术,希望你们别太介意题目中提到的那个网站目前尚未完成。

但是如果你非得看看该网站的实际情况以判断我后面的观点是否有价值,点击进入 Know it all

不出意外,它的打开速度应该极快(对于国内用户,请把墙的影响考虑在内,不要让它影响你的判断),毫无疑问,它会打消你们对于我能力的质疑。如果你觉得我不够谦虚,那是因为我就是这么牛逼。

我来给网站功能做个简单推销:“你有没有想过对于 web 到底有哪些东西是你所不清楚的?它的哪些部分成功的躲过你的注意?难道你不曾想过列个清单然后看看勾出那些东西是你真正理解的部分,然后开始学习剩下那些有趣的东西?”

如果你还纠结于我为什么要在该网站还没完工之前写这篇文章...因为我需要反馈。影响性能的因素面很广,也有很多奇淫邪技可以提升页面速度。我希望能有一些我并不知道的东西,热烈欢迎给我提出各类建议。如果你迫不及待的想深入挖掘该站实现代码,项目在这

聊聊速度

什么?你想要看图?

你想要别具一格的图?

我把 repeat view 放在上面。因为这是我的东西我想怎么弄就怎么弄。

这个对比公平公正吗

不,当然不了。这些网站功能完全不同,要求它们有相同的加载时间显然是不理智的。但是它们都是一些拼尽全力提升性能试图赶超 google 首页加载速度的网站。

为什么它看起来没那么惊艳

如果你不巧觉得被它震惊了,建议你(暂时)先冷静一下。是的,我不需要从数据库中读取产品内容,或者进行登录操作、加载那些要跳转40次以加载一个flash文件的第三方广告,甚至我连图片都没有。哦,还有,我的页面数——一个。

为什么它并非看起来的那么平庸

在页面加载完成并展示到你面前时,它已经下载并解析了一个长达 75,000 行的 JSON 文件。最终得到的树——如果全展开的话——足有 9,986 行(是的,我把整个都规范搬过来了)。

还有,我使用了一个库。加载库是一件很慢的事——不管它本身有多快。

一路上我纠正了很多观点和态度,学到了不少东西,整理之后,四舍五入一下有 10 点。两个月前我发现其中7个非常有用,另外3个纯粹垃圾。

我是不会告诉你那3点的。

1 不要做一个慢的网站

最近我听一个非web开发者跟我说:“前几天我上 mecedes.com ,它慢的要死,他们是怎么做到让一个网站那么慢的?”。

我当时刚弄了一个纹身,满嘴谈论 upper register (慎点),所以回答的可能有点不清楚。但是大概想法是要做个龟速的网站很容易,不尽力将它变快就行了。

(是只扛着机关枪的熊。)

这是个好消息,也就是说如果你尽力让它变快,你最终将自动得到一个飞快的网站。你要不停的尝试,就像做混蛋一样,稍做练习就会很容易。

在这个网站的实现过程中,每隔一段时间我就考虑一下在做的部分对于性能的影响。对于这个应用中使用到的几乎每个库,我都衡量了三个尺度:

  • 到页面中第一次能看见有用信息的时间
  • 到可以进行交互的时间
  • 展开一个 DOM 节点时间

如果一个库对性能有负面影响,抛弃。比如有一次,脑子发热使用 lodash 对 7,5000 个属性的 JS 对象执行 deepClone 操作。最后改用 Immutable.js,性能显著提升。

我现在使用 React,有一次引入了 classnames 库,然后我照常去测量那三个尺度的性能...没影响,嗯, classnames,你幸免于难。

引入新的库或者进行重大修整时测量花个5分钟使用这种方式测量它们对性能的影响对于提升网站性能其实是个很划算的买卖。

2 移动优先。我是说,真的做一个移动端版本

“移动端优先” 有两种策略。

第一种(也是在这个项目之前我一直使用的),你坐在27寸的显示器前,就像看页面的 imax 版那样,用着非常好的显卡还有用不完的内存,使用 media query 指定 min-width,然后告诉朋友我在做移动优先。

这个项目中,我做了 真正 的移动优先。即开发时,使用移动设备运行页面。我这样做直到性能和 UI 已经满足我的需求,然后才转到电脑上继续工作。

你一定想不到在一个性能牛逼的电脑上做出一个速度很快的网站有多么简单!

(这个故事并没有说的那么简单,项目开始时,我延续了以前的坏习惯,中途才突然顿悟然后开始默认在移动设备上进行开发测试。)

如今,仅在提供手机版本的网站不切实际,除此之外,我们要持续测试性能,这需要一个稳定的量尺,看过这个视频你会发现移动设备不适合作为稳定的测试平台。

进行性能测试时,注意使用 Chrome DevTools 提供对 CPU性能和网络给予适当限制。我将 CPU 调低10倍,网络改为“3G”网络。我知道这种限制下它可能还是比手机的平均性能要高,但是我还不想矫枉过正,在极端环境中去无谓地折磨自己。

光点头同意这种方式并没有什么用,关键是真的这样去实践。

一个令我吃惊的发现:我的电脑使用 i7 处理器,移动设备是一台全新的 Pixel XL——世界上最快的手机。你觉得该手机的性能大概是电脑的百分之多少呢?80%?60%?至少 10% 以上?

错了!只有 10%。我面前这部 $1,400 的手机性能只有 i7 的 1/10 !

这是点击响应速度为 20ms 和 200ms 的差别;渲染一帧时间为 16ms 与 160ms 的差别。(麻烦在评论中继续把这个排比补全)

3 疯狂的测试性能

我的自负让我来到这里跟你们炫耀(它已经溢出来了)。一旦在某个性能测试网站上获得了高分,我会跑到所有其它测试站点听它们测试完成后自动化的称赞。

lighthouse 中测试我的站时,分数停在了 97。我他妈的剩下的那三分呢!


奥,它告诉我有个输入有 285ms 的延迟。如果这是真的,那也真是骇人听闻了。但我知道它只需花费 20ms。

显然 lighthouse 那帮人搞错了什么,一群白痴。

冷静了一下,我不情愿的承认也许还是需分析一下究竟是什么情况,尽管显然我是对的,Google 写的算法明显搞错了。

然后我开始了拉低 CPU 速度进行测试那一档子流程,果然,我感觉应该瞬时完成的部分现在有了 200+ms 的延迟。

我进行了仔细的分析,发现延迟的大部分时间耗费在 React 中。我已经做了 React 性能相关所有的最佳实践,也没有做“无用的更新(wasted update)”。

我甚至对 low-cardinality components(译者注:读者中有知道什么意思的,麻烦告知一下)做了缓存优化。(我不知道 cardinality 是什么意思,但我觉得我用的没错。)

这里我得指出我是 React 的超级粉丝,我有三只宠物都是以 React 命名的(一只狗,两只雪貂),最后不得已不这样做了是因为它们一个个都死了,我不禁想问题肯定出在这个名字上。

对 React 的这种热爱让我在找其他前端框架时就像在上 Ashley Madison(Ashley Madison是一家专门为已婚人士提供交友、约会服务的社交网站。网站的口号是:“人生短暂,偷情无限!”)。但是性能高于忠贞,我沉重的拥抱了 Preact

最开始我尝试的事 preact-compat。花了大概 15 分钟就迁移过去了。性能提升明显,真棒~。

我把这事跟 Preact 的开发者说了一下,他建议我试一下完整版的 Preact。果然又快了不少。

你想要图了吧?

另一个人发推提醒我该试一下 Inferno。然后我把应用转成 inferno 的试图再挤出一点性能。

什么?图?要绿色的?



好吧,我尝试过了,Inferno 的确很快,但是没有 Preact 快,所以我回滚了代码。

注意,打算遗弃做过的东西并重新做的时候不要害羞。但是还是要慎重的。

每当我不大乐意重写时,我就想反正活着本来就没什么意义,而且不幸电脑奔溃什么都留不下。 ——给你的小小的建议。

然后,看看结果:



然后我在 yslow 上测了一下。我想毫无疑问应该得最高分,然后他们因为我的 DOM 节点过多给了我一个 D!这简直是无理取闹,我知道应该使用多少 DOM 节点,没人能告诉我应该怎么做,我才是自己的老大。

显然 yslow 的那帮人全是蠢货。

然后我不情愿的想也许可以减少要渲染的 DOM 节点的数量。所以我修改了默认渲染的分支。

图?

这次蓝底白字?


说实话,这个结果还挺让人吃惊的。

谢谢了,yslow,你们给出的建议还是挺有用的。

上面是三条建议是垃圾的三条。剩下的都是金子。

4 客户端渲染非常昂贵

客户端渲染(Client Side Rendering,CSR)——我称之为“把钱点着然后扔到河里”——有其用处,但是对于这个网站,不适合这样。

这个页面不包含任何用户相关的信息,可以直接将一份相同的 HTML 发给每个人。而且在客户端中计算量比较大,使得 CSR 更为耗时间。CSR 显然不是将页面展示出来最快的方式。

对于那些用于给公司带来收入的——需要被持续维护——的页面,下面是我给出的建议:

  • 分析一下有多大流量来自 Bing(就我的雇主而言,1.6%)
  • 是的,Bing。因为他们在建立索引时不会执行 JS(因此他们不会索引 CSR 页面)。
  • 将你雇主一年的收入乘以 1.6%。
  • 问问你的雇主他们是否愿意让那么多的现金流入竞争对手哪里,因为你们不会出现在 Bing 的搜索结果中。

是的,这里有一些逻辑漏洞,呃,嗯。但你明白我想表达什么。

我岔题了。

直接把服务器渲染好的 HTML 页面发给用户。

5 不要在服务器端渲染 HTML

显然是自相矛盾了,胡说些什么?不在服务端渲染 HTML 怎么提供 HTML 页面?这是上个世纪90年代人做的事情...

React(还有比它更快的表亲们)能够在几十毫秒内将渲染好 HTML。(有人统计过 PHP 或者 JSP 要花多长时间吗?我对这个比较很感兴趣。)这意味着单核每秒只能给 50 个人提供服务;额外的请求需要排队,这可不妙。

就我这个小站而言,我给每个人发送相同的 HTML。如果你的站点和我一样,那么你不需要让你的服务器对每个请求进行 HTML 的渲染然后发送页面、CSS和JS文件。你可以在 编译时生成 HTML 页面。然后将所有东西打包静态托管(或者放在一个好的 CDN 上)。Github、Firebase 或者其他好人愿意托管你的静态资源。

这种情况并不多见所以如果你跳过这条建议我也不会太失望。但是如果你有任何可以编译时生成的页面(比如 LinkedIn,Paypal,Github 的主页),使用这种方式。

这条是我后来突然想到的,当时我在浏览一篇博客,然后想到点近该博客只花了 96ms。(然而我还时认为我的网站是世界上最快的——现实也不能改变这点。)


我深挖了一下发现该博客托管在 Firebase 上。我想我会试试他们的服务。

但那意味着我得在编译期生成页面。

如果 React 能够将生成的 DOM 输出成字符串就好了。那我就可以在构建脚本中把它存为 HTML 文件。

对那些不知道的人悄悄说一句,很有意思的是,React 的确有个叫 renderToString 的方法。

(既然到这一步了,在保存 HTML 之前不妨先用 minifier 处理一下。)

6 內联

每当我坐下来尝试想出到底应不应该使用內联 CSS 时,总是得出一个结论——得看情况...

如果像 facebook.com 一样,99.9% 的页面访问来自回头客,最好将 CSS 文件分开便于浏览器进行缓存。对于丧葬公司首页来说,可能回头客没那么多,也许使用內联 CSS 可以帮你减少一点请求。

如果你的生活太过一帆风顺,想制造点泪水和挫折,可以尝试一下给一些元素添加內联样式,其他样式放在单独的 CSS 文件中。

我个人的原则是(所以现在我不需要考虑这个问题):如果将 CSS 放进 HTML 且保持最终所有东西控制在 14KB,那么就使用內联。(不知道为什么是 14?读这个

我的 CSS+HTML(minified)为 3.5KB,所以无需考虑——我使用了內联样式。

7 预加载,然后加载

<script> 应该像我们的脚一样,放在 <body> 的尾部。奥,我突然意识到为什么大家叫它 footer 了!OMG,header 也是!哎呀卧槽,还有 body! 炸裂了我的脑子。

[五分钟后...]

HTML侠


有时候我觉得自己太容易分心了,有一次有人告诉我我的脑子不仅在神游,它是穿着短裤乱蹦跶。

你们在这干嘛呢?

哦!我在教你们提升站点性能。

React SSR(服务端渲染,Server Side Rendering)中通用的模式是这样的:

  1. 在服务器上将数据传递给要渲染的组件然后生成 HTML;
  2. 将数据写入 HTML 文档,如 window.APP_DATA = data;;
  3. 将渲染好的 DOM(基于该数据)和数据发送给客户端;
  4. 在浏览器中读取 window.APP_DATA 然后启动应用。

这种方式可能会增加需要传输的HTML文档大小,不过这并不会影响到首屏用户体验,只会让用户真正的可操作时间点后延,毕竟JavaScript的传输开始时间被推迟。如果你的页面在不加载 JavaScript 时能显示大部分有用内容,页影响不大。但是我的那个站在没有 JavaScript 的话就一脸懵逼。

所以,我想要它这样:

  1. 尽早下载数据(不阻塞 HTML);
  2. 尽早下载应用 JavaScript 代码(不阻塞 HTML);
  3. 当数据和JS下载完成、页面被解析好、JS被运行,最后页面就活过来了。

我可以在 JavaScript 中使用各种风骚的技巧实现这些,但是有一个更好的方法。至少它能满足要求,但我感觉可能哪个地方有点不对。

浏览器专家们,一起来找茬:

  1. 在 <head> 中为 JSON 和 JS 添加 <link rel="preload" ...>(有些浏览器暂不支持 prefetch ,所以我还添加了 prefetch)
  2. 在 body 的末尾添加 script。
  3. 当JS被执行时,调用 fetch() 下载 JSON 文件,然后 .then() 中完成 React 应用的渲染。

如下:


一旦资源下载完成,之后对其的下载不再需要访问网络。网络请求如下:


这样看来,除非我遗漏了什么,不然没理由不在页面的开始预加载所有东西。

一个插曲:我想把 JSON 文件的 hash 添加到名字中以永久缓存时,坏了自己的规则,像个傻逼一样到 npm 中找了半天,突然发现 Node 内建了一个 crypto 库,果断存到 gist 中去:


const fileHash = crypto.createHash('md5').update(fileContents).digest('hex');

该把它放到 npm 中起个名字叫 hashr

8 奖励好的行为

用户中使用 Chrome、Edge和Firefox的都是好人。创建一个 30KB 大小的 polyfill 发给他们对他们公平吗?不,不公平。

这个项目中,我创建了一个单独的 polyfill 文件,然后在需要的时候加载:


var scripts = ['app.a700a9a3e91a84de5dc0.js']; // script for all users

var newBrowser = (
  'fetch' in window &&
  'Promise' in window &&
  'assign' in Object &&
  'keys' in Object
);

if (!newBrowser) {
  scripts.unshift('polyfills.a700a9a3e91a84de5dc0.js'); // script for the less fortunate
}

scripts.forEach(function(src) {
  var scriptEl = document.createElement('script');
  scriptEl.src = src;
  scriptEl.async = false;
  document.head.appendChild(scriptEl);
});

使用 webpack 生成这两个包出奇的容易:


config.entry = {
  app: [
    path.resolve(__dirname, `../app/client/client.jsx`),
  ],
  polyfills: [
    `babel-polyfill`,
    `whatwg-fetch`,
  ],
};

它将需要的编译的东西从 90KB 减少到了 60KB。

你可能也意识到了现在为止我还没提过下载大小相关事宜。因为文件大小无关紧要。

如果你要测量站点“到可交互花费时间”,那么你已经把下载时间考虑在内了;它包含了下载和解析的时间。

下面是应用+React+polyfill的大小、去掉polyfill的大小、使用 Preact 后大小的对比:


如果你想做更细致的裁剪,为不同浏览器定制不同的 polyfill,polyfill.io 已经帮你做了。这供一些特殊用户使用,下面是为什么你不需要使用它的原因:

  1. 任何时候,它们的实现出现问题,你的整站可能崩溃。它又可能在一个你不常用的浏览器中不能正常运行。也许你的站现在在某人的浏览器中已经不正常了,你怎么追踪?
  2. 它非常快。但毕竟还是阻塞页面的,如果它加载花费一秒,这期间你什么都不能干。

(我觉得没提 Safari 有点过分——Safari 10 对 JavaScript 支持非常好——但是没有 fetch 所以在我眼中不算现代浏览器)

9 Service workers:就像高中时的我

(酷且平易近人)

很长一段时间内我都拖着没有学习 service workers。我想总有一天我要花上 400 小时看看它到底是怎么实现的。直到这天,我决定制作这个星球最快的网站,我想时候到了。

五个小时后,我都搞明白了。一点没骗你。而且其中4小时35分钟都是在做错事。简而言之:

  1. 构建脚本完成本职工作生成一堆文件到一个叫 public 的目录中(包括我的 index.html),没什么特别的;
  2. 然后我让 Google 的 sw-precache 库创建一个 service worker 文件,它会缓存该目录中所有的文件,使得我的应用能够离线运行;
  3. 最后我的客户端代码注册到 sw-precache 创建的那个 service worker;
  4. 然后就没有然后了。

总共耗费16行代码。其中13行是构建脚本:


swPrecache.write(path.resolve(__dirname, `../public/service-worker.js`), {
  cacheId: `know-it-all`,
  filename: `service-worker.js`,
  stripPrefix: `public/`,
  staticFileGlobs: [
    `public/app.*.js`, // don't include the polyfills version
    `public/*.{html,ico,json,png}`,
  ],
  dontCacheBustUrlsMatching: [
    /\.(js|json)$/, // I'm cache busting js and json files myself
  ],
  skipWaiting: true,
}, (err) => {
  if (err) {
    reject(err);
  } else {
    resolve();
  }
});

(这里没有缓存 polyfill 是因为那些需要 polyfill 的浏览器不支持 service worker。)

然后客户端代码添加三行用于加载 servier worker :


if (`serviceWorker` in navigator) {
  navigator.serviceWorker.register(`service-worker.js`);
}

网站加载一次后,之后使用就不再需要网络。新版本可用时,如果接入了网络,刷新一下,新版本在后台被自动加载。

朋友们,这是未来啊。好吧,它早些年就已经出来了,只是我最近才学它。朋友们,广而告之, 都去用 service worker 吧

很不幸,你们中 50% 使用 Safari/iOS 阅读这篇文章的没有 service worker 支持。Apple 肯定再加紧实现它了,不然有一天你想上网快一点只能用安卓了。

10 计算机自带好看的字体

谈到 web 字体时,我总是很撕裂,它们令人痛苦、吃性能。

我细细想了这个问题,得到了一个解决方案,惊人的简单,它包含四个部分:

  1. macOS 自带好看的字体
  2. Android 自带好看的字体
  3. Windows 自带好看的字体
  4. iOS 自带好看的字体

那么为什么不使用它们呢?我选择了 Calibri Light, Roboto and Helvetica Neue。如果你坚持要在所有设备上使用相同的字体,那么你没救了。

下面是我认为的每个网站都需要的东西,简单,无需网络请求:


body {
    color: #212121;
    font-family: "Helvetica Neue", "Calibri Light", Roboto, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    letter-spacing: 0.02em;
}

注:最初这里添加了 text-rendering: optimizeLegibility ,评论中有人指出有导致性能问题。

显然这个人是个傻瓜朋克Draft Punk)。

然后,很不情愿的,我做了一些测试看看有什么不同:


Jacob Groß! ,谢谢!

11 永不言弃

这篇博客放出来已经有一段时间了,有很多反馈,我还在不断优化。

我的 app.js 大小已经降到了约 28KB,我在想它由哪些部分组成。一番侦查后发现 ImmutableJS 占据了 19KB!一个小小的库竟然占据了整个应用大小的三分之二!


我仅用了 ImmutableJS 中很少的部分,我觉得我可以自己实现那个部分。花了几个小时替换掉了 ImmutableJS,性能提升按百分比来说大于之前做的所有改变。减少了 60% 的加载时间!


并不是因为 ImmutableJS 有多慢,只是任何 19KB 的 JavaScript 代码都需要花点时间去解析。

睡着了的醒醒!我们快结束了。

写一个快的网站就像养一条小狗,需要持之以恒的耐力。你可以很注意的优化所有代码到最优、最小,但是一次懒惰然后使用了一个 11KB 的库来格式化日期、让狗狗在床上拉了一次粑粑,就白瞎了那么多幸苦的劳动、得去洗半天的被子。


延伸阅读: