APP下载

如何将Web主页效能提升十倍以上?

消息来源:baojiabao.com 作者: 发布时间:2026-03-21

报价宝综合消息如何将Web主页效能提升十倍以上?

最近,我们将 Universe.com 主页的效能提升了十倍以上。在本文中,我们将解析实现这一重大改进的具体技术手段。

但在开始之前,让我们先对网络效能的重要意义进行一番论证(博文末尾提供相关案例研究连结):

使用者体验: 糟糕的效能可能导致响应失败,从 UI 与 UX 的角度来看,这可能会引发使用者的沮丧情绪。

客户转化与收入: 网站速度缓慢通常会导致客户流失,并对转化率与收入产生负面影响。

SEO: 从 2019 年 7 月 1 日开始,Google公司开始在全部新网站上预设启用移动优先索引。如果网站在移动装置上执行缓慢,且没有针对移动装置进行内容格式调整,那么网站的搜寻排名将会降低。

在本篇文章中,我们将简要介绍以下几大有助于我们提高页面效能的主要领域:

效能测量: 实验室与现场工具测量。

渲染: 客户端与服务器端渲染、预渲染以及混合渲染方法。

网络: CDN、快取、GraphQL 快取、编码、HTTP/2 以及 Server Push。

浏览器中的 JavaScript: 资料包大小预算、程式码拆分、async 与 defer 指令码、影象优化(WebP、延迟载入、渐进式设计)以及资源提示(preload、prefetch 与 preconnect)。

这里再介绍一点我们的情况:我们的主页由 React(TypeScript)、Phoenix(Elixir)、Puppeteer(headless Chrome)以及 GraphQL API(Ruby on Rails)构建而成。以下为主页在移动装置上显示的效果:

Universe 主页与浏览效果

效能测量 没有资料作为支援,一切意见都将毫无意义。

—— W. Edwards Deming

实验室工具 实验室工具能够立足受控环境从预定义的装置及网络设定中收集资料。利用这些工具,我们能够轻松除错任何效能问题并实现良好的可重复测试。

Lighthouse 就是一款立足本地计算机对 Chrome 内网页进行审计的出色工具。其能够提供一系列关于如何提高效能、可访问性以及搜索引擎优化的实用性提示。下面,我们来看模拟高速 3G 加 4x CPU 场景下的 Lighthouse 效能审计报告:

之前与之后:首屏内容填充(简称 FCP)效能实现 10 倍提升

然而,单纯使用实验室工具也会带来不少弊端:这类工具不一定能准确反映出终端使用者所面临的装置、网络、位置以及多种其它现实因素造成的效能瓶颈。正因为如此,我们才需要配合现场工具进行补充。

现场工具 现场工具允许我们模拟并测量使用者的真实页面负载。目前有多种服务可帮助大家从实际装置当中获取真实效能资料:

WebPageTest — 允许使用者立足不同位置上的实际装置对不同浏览器进行效能测试。

Test My Site — 使用 Chrome 使用者体验报告 (CrUX) 功能,并以 Chrome 使用情况统计为基础;这款工具公开可用且每月更新一次。

PageSpeed Insights — 将实验室(Lighthouse)与现场(CrUX)资料加以结合。

WebPageTest 报告

渲染 内容的渲染可通过多种方法实现,其中每一种都拥有独特的优势与缺点:

服务器端渲染 (SSR) 是指在服务器端为浏览器提供最终 HTML 文件的过程。优势:搜索引擎可以直接抓取网站而无需执行 JavaScript(SEO)、快速初始页面载入、程式码仅存在于服务器端。短板:非富网站互动、整页重新载入、浏览器功能受限。

客户端渲染是指利用 JavaScript 在浏览器当中进行内容渲染的过程。优势:富网站互动、在初始载入后可快速呈现路由变更内容、支援现代浏览器功能(例如配合 Service Workers 实现离线支援)。短板:SEO 友好性差、初始页面载入缓慢、通常需要在服务器端实现单页面应用程序(SPA)与 API。

预渲染类似于服务器端渲染方法,但渲染会提前发生在构建时而非执行时。优势:built 静态支援档案通常比服务器执行方法更简单、SEO 友好性高、快速初始页面载入。短板:需要在执行任何程式码变更时提前进行完整页面重新载入、非富网站互动、浏览器功能访问限制。

客户端渲染 以前,我们将自己的主页与 Ember.js 框架一同实现为采用客户端渲染方法的单页面应用。但这种作法的一大问题在于,我们的 Ember.js 应用程序包过大。这意味着在浏览器下载 JavaScript 档案并对其进行解析、编译与执行的过程中,使用者只能对着空白屏幕发呆:

最要命的空白屏幕

因此,我们决定利用 React 重构应用当中的某些部分。

我们的开发人员已经非常熟悉 React 应用程序的构建方法(例如嵌入式功能部件)。

我们已经拥有多个 React 元件库可在多个专案间随意共享。

新的页面中将可包含一些互动式 UI 元素。

庞大的 React 生态系统能够提供多种工具方案。

利用浏览器中的 JavaScript,我们可以通过多项强大功能构建起渐进式 Web 应用。

预渲染与服务器端渲染 客户端渲染应用程序的具体构建——例如采用 React Router DOM,仍然会带来与 Ember.js 相同的问题。JavaScript 需要占用大量资源,而且访问者需要经历一段首屏内容填充周期才能看到实际内容。

因此在决定使用 React 之后,我们开始尝试其它潜在的渲染选项,以确保浏览器能够更快地完成内容渲染。

使用 React 时的常规渲染选项

Gatsby.js 允许我们利用 React 与 GraphQL 构建预渲染页面。Gatsby.js 是一款强大的工具,能够直接提供多种效能优化方案。然而,预渲染方法并不适合我们的需求,因为我们的网站中可能存在无数包含使用者生成内容的页面。

Next.js 是一套高人气 Node.js 框架,允许使用者通过 React 实现服务器端渲染。然而,Next.js 设定了太多条条框框,要求使用者使用它提供的路由机制以及 CSS 解决方案等等。另外,我们的现有元件库是专为浏览器构建的,与 Node.js 并不相容。

因此,我们打算尝试一下混合方法,即发挥每一种渲染选项中的独特优势。

执行时预渲染 Puppeteer 是一套 Node.js 库,允许使用者使用 headless Chrome。我们希望尝试利用 Puppeteer 在执行时当中实现预渲染。这代表着一种有趣的混合方法:利用 Puppeteer 进行服务器端渲染,同时利用 hydration 进行客户端渲染。感兴趣的朋友可以点选此处检视Google提供的关于如何利用 headless 浏览器进行服务器端渲染的相关提示。

利用 Puppeteer 对 React 应用程序进行执行时预渲染

这种方法具备以下优势:

允许 SSR,因此有利于 SEO 优化。抓取程式不需要执行 JavaScript 即可看到网页内容。

允许一次性构建起简单的浏览器 React 应用程序,而后将其同时用于服务器端与浏览器内。这将同时提高浏览器应用与 SSR 的速度表现,一举两得。

利用 Puppeteer 在服务器端渲染页面,在速度上一般快于在终端使用者的移动装置上进行渲染(前者网络连线更强、硬件配置也更高)。

Hydration 允许我们构建起富 SPA,并可访问 JavaScript 的浏览器功能。

我们不再需要预先了解所有可能被呼叫的页面,也不需要预先进行渲染。

但在采用这种方法的过程中,我们也遇到了一些挑战:

吞吐量是最主要的问题。每项请求都会在单独的 headless 浏览器程序当中占用大量资源。虽然我们可以使用单一 headless 浏览器程序并在其中的各个选项卡内执行多项请求,但使用多个选项卡仍会降低整个程序的效能水平。

利用 Puppeteer 的服务器端渲染架构

• 稳定性。对众多 headless 浏览器进行规模伸缩,同时保持程序不致过热并实现负载均衡绝对是一项高难挑战。我们尝试了不同的托管方法,包括在 Kubernetes 丛集内进行自托管,以及利用 AWS Lambda 与 Google Cloud Functions 实现无服务器计算。我们注意到,后一种方法在配合 Puppeteer 时存在一些效能问题:

AWS Lambdas和GCP函式的Puppeteer响应时间

在配合 AWS Lambdas 与 GCP Functions 时,Puppeteer 的响应时间结果随着我们对 Puppeteer 熟悉程度的逐步提升,我们开始对初始方法进行迭代(后文将具体说明)。我们还进行了其它一系列有趣的实验,希望通过 headless 浏览器渲染 PDF。再有,即使不编写任何程式码,我们也能够利用 Puppeteer 自动进行端到端测试。而且除了 Chrome 之外,Puppeteer 现在还支援 Firefox 浏览器。

混合渲染方法 在执行时中使用 Puppeteer 并非易事。正因为如此,我们才决定在构建时中加以使用,同时配合一款工具用于在执行时内从服务器端获取使用者生成的实际内容。很明显,这款工具必须拥有比 Puppeteer 更强大的稳定性与吞吐能力。

我们决定使用 Elixir 程式语言。Elixir 看起来与 Ruby 非常相似,但执行在 BEAM(Erlang VM)之上。顺带一提,BEAM 专门为构建高容错、高稳定性系统而生。

Elixir 采用 Actor 并发模型。每个“Actor”(即 Elixir 程序)的内存占用量都非常有限,仅为 1 到 2 KB。这意味着系统将能够同时执行成千上万个独立的程序。Phoenix 则是一套 Elixir Web 框架,能够支援高吞吐量,并允许开发者在各个独立的 Exlixir 程序当中处理各项 HTTP 请求。

我们将上述方法结合起来,充分利用其各自优势,希望能够切实满足自身需求:

Puppeteer 用于实现预渲染,Phoenix 则用于实现服务器端渲染

Puppeteer 在构建时中按照我们预期的方式对 React 页面进行预渲染,并将结果储存为 HTML 档案(来自 PRPL 模式的 app shell)。

我们可以继续构建一款简单的浏览器 React 应用程序,并在无需等待终端使用者装置 JavaScript 处理过程的同时获得快速初始页面载入效果。

我们的 Phoenix 应用负责实现页面预渲染,并以动态方式将实际内容注入至 HTML。这就使得内容的 SEO 友好性大幅提升,让按需处理大量多种页面成为可能,并显著降低了扩充套件难度。

客户端接收并立即开始显示 HTML,而后由 Hydration 将 React DOM 状态持续作为常规 SPA。如此一来,我们就构建起了高度互动的应用程序,并可访问各项 JavaScript 浏览器功能。

利用 Puppeteer 建立预渲染架构,利用 Phoenix 进行服务器端渲染,React 则在客户端上实现 hydration

网络 中标题内容交付网络 (CDN) 利用 CDN 可帮助我们实现内容快取,并加速其在全球范围内的交付速度。我们选择了 Fastly.com,其目前处理著全球超过 10% 的请求总量,并得到 GitHub、Stripe、Airbnb 以及 Twitter 等诸多厂商的青睐。

Fastly 允许我们编写定制化快取,并可利用 VCL 配置语言建立路由逻辑。下面,我们将具体聊聊基础请求流如何根据路由、请求头等因素分步起效:

VCL 请求流

提高效能的另一个选项是配合 Fastly 在边缘位置使用 WebAssembly(WASM)。大家可以将其视为一种无服务器模式,只是处于边缘位置;所使用的语言则包括 C、Rust、Go 以及 TypeScript 等等。Cloudflare 就拥有一个类似的专案,用于在 Workers 上支援 WASM。

中标题快取 尽可能多地利用快取处理请求是改善效能水平的关键所在。立足 CDN 层级进行快取,将能够更快地为新使用者提供响应。而通过传送 Cache-Control 头进行快取,则可加快浏览器中重复请求的响应速度。

大多数构建工具(例如 Webpack)允许使用者向档名当中新增杂凑值。由于指向这些档案的任何变更都会产生新的输出档名,因此大家可以安心将档案新增至快取当中。

通过 HTTP/2 进行档案快取与编码

GraphQL 快取 传送 GraphQL 请求的一种常见方法,就是利用 POST HTTP 方法。而我们选择了立足 Fastly 层级对部分 GraphQL 请求进行快取:

我们的 React 应用会标注出那些可进行快取的 GraphQL 查询。

在传送 HTTP 请求之前,我们以请求本体为基础构建一条附加 URL 引数,其中包含 GraphQL 查询与变数(我们配合 Apollo Client 使用自定义 fetch)。

在预设情况下 ,Varnish(与 Fastly)会使用完整的 URL 作为快取金钥的一部分。

这意味着我们可以通过请求本体当中的 GraphQL 查询不断发送 POST 请求,并在无需接触服务器的前提下立足边缘位置完成快取。

利用一条 SHA256 URL 引数传送 POST GraphQL 请求

以下是其它一些值得参考的潜在 GraphQL 快取策略:

服务器端快取:立足解析器层级或者通过模式标注对全部 GraphQL 请求进行快取。

利用持久化 GraphQL 查询并发送 GET /graphql/:queryId 以使用 HTTP 快取机制。

利用自动化工具(例如 Apollo Server 2.0)或者 GraphQL 专用型 CDN(例如 FastQL)实现不同 CDN 的整合。

编码 目前,所有主流浏览器都支援利用 gzip 加 Content-Encoding 标头进行资料压缩。这意味着面向浏览器的传送资料量更低,从而带来更快的内容传递速度。此外,如果浏览器支援,大家也可以尝试使用效率更高的 brotli 压缩算法。

HTTP/2 协议 HTTP/2 是 HTTP 网络协议的新版本(DevConsole 中简称为 h2)。由于存在着以下几项与 HTTP/1.x 版本间的显著差别,切换至 HTTP/2 能够带来效能提升:

HTTP/2 为二进位制,而非文字式。因此其解析效率更高,也更加紧凑。

HTTP/2 具有多路复用属性,这意味着 HTTP/2 可以通过单一 TCP 连线传送多项请求。如此一来,我们就不必担心每主机浏览器连线限制以及域名分片等问题。

其利用标头压缩机制减少请求 / 响应的实际体积。

允许服务器主动推送响应。这项功能拥有诸多有趣的实际应用方式。

HTTP/2 Server Push 由于给现有工具及生态系统(例如 rack)引入了一系列颠覆性的变更,很多程式语言与库并不能完全支援 HTTP/2 的全部功能。但即便如此,我们仍然可以在部分合适的场景中使用 HTTP/2。举例来说:

利用 HTTP/2 在常规 HTTP/1.x 服务器之前设定一套 h2o 或者 nginx 代理服务器。Puma 与 Ruby on Rails 能够传送 Early Hints,从而在一定的限制条件下启用 HTTP/2 Server Push。

利用支援 HTTP/2 的 CDN 交付静态资产。例如,我们可以使用这种方法将字型以及一部分 JavaScript 档案推送至客户端。

HTTP/2 推送字型

对 JavaScript 以及 CSS 的推送功能同样非常实用。但请注意不要过度推送,您可点选此处了解一些相关问题: https://jakearchibald.com/2017/h2-push-tougher-than-i-thought/

浏览器中的 JavaScript 包大小预算 JavaScript 效能优化中的头号规则就是,不要使用 JavaScript。

—— 我自己

如果您已经拥有现成的 JavaScript 应用程序,那么设定预算规则能够提高包大小的可见性,同时确保全部内容都可容纳于同一页面当中。超出预算后,开发人员则需要谨慎考虑并尽量防止规模进一步增长。以下是预算设定方面的相关示例:

根据您的实际需求或推荐值设定数值。例如,不得大于 170 KB 否则压缩 JavaScript。

利用现有包大小作为基准,或者尝试对其进行削减——例如下调 10%。

尝试让网站拥有高于竞争对手的速度,并以此为依据设定预算。

您可以使用 bundlesize 工具包或者 Webpack 效能提示与限制进行预算跟踪:

Webpack 效能提示与限制

消除依赖性 Sidekiq 曾在一篇博文中提到:“程式码越少,执行速度越快。程式码越少,bug 就越少。程式码越少,占用的内存量就越低。程式码越少,理解起来就越轻松。”

遗憾的是,实际 JavaScript 场景中往往存在着不计其数的依赖关系。您可以试试: ls node_modules | wc -l。

在某些情况下,新增依赖性是种必然的选择。在这种情况下,依赖性的包大小应该被视为决定您实际工具包选择的重要依据。我强烈建议大家使用 BundlePhobia:

BundlePhobia 能够提示将 npm 工具包新增至您资料包中带来的实际成本

程式码拆分 使用程式码拆分是另一种能够显著提高 JavaScript 效能的好办法。其本质在于分解程式码片段并仅向用户交付当前所需要的部分。以下是关于程式码拆分的相关示例:

在不同的 JavaScript 程式码块间分别载入路由机制。

拆分那些在页面中无法立即显示的部分,例如弹出框以及页面下方的页尾。

Polyfills 与 ponyfills 可支援全部主流浏览器当中的各最新浏览器功能。

利用 Webpack 的 SplitChunksPlugin 防止程式码重复。

按需定位档案,以避免一次性发送所有受支援的语言。

您可以利用 Webpack 动态汇入以及 React.lazy 配合 Suspense 实现程式码拆分。

利用动态汇入以及 React.lazy 配合 Suspense 实现程式码拆分。

相较于预设汇出,我们构建的函式可取代 React.lazy 以支援点名汇出。

Async 与 defer 指令码 目前,全部主流浏览器皆在 script 标签上支援 async 与 defer 属性:

载入JavaScript的不同方式

几种不同的 JavaScript 载入方式:

内联指令码适用于载入小体积、高关键度 JavaScript 程式码。

当您的使用者或者任何其它指令码(例如分析指令码)不再需要某些特定指令码时,大家可以将 async 与这些指令码配合使用以避免 HTML 解析阻塞。

从效能角度来看,将 defer 与指令码配合使用能够有效提升非关键 JavaScript 程式码的抓取与执行效率,且避免发生 HTML 解析阻塞。此外,这种作法还能够在呼叫指令码时保证执行顺序,从而确保不同指令码间存在依赖性时实时与预期相符的执行效果。

下成来看 head 标签下不同指令码间的视觉化差异:

几种不同的指令码抓取与执行方式

影象优化 虽然与 100 KB 的影象相比,100 KB 的 JavaScript 程式码明确会带来更高的效能成本,但我们同样有必要重视对影象内容的优化调整。

削减影象大小的有效手段之一,是在适用的浏览器当中采用更加轻量化的 WebP 影象。对于那些无法支援 WebP 的浏览器,大家则可以采取以下几种策略:

回退至常规的 JPEG 或者 PNG 格式(某些 CDN 会根据浏览器的 Accept 请求标头自动执行)。

在检测浏览器的支援情况后,载入并使用 WebP polyfill。

利用 Service Workers 监听 fetch 请求,并在支援时利用 WebP 变更实际 URL。

WebP 影象

仅当影象位于检视当中或者附近时才进行内容载入,堪称多影象初始页面载入过程中效果最显著的提速手段之一。您可以在受支援的浏览器当中使用 IntersectionObserver 功能,也可以利用其它一些替代性工具实现相同的结果——例如 react-lazyload。

在滚动过程中进行影象的延迟载入

其它一些影象优化策略还包括:

降低影象质量以减小体积。

调整大小并载入最小影象。

利用 Srcset 影象属性自动在高分辨率显示器上载入高质量影象。

利用渐进式影象快速显示影象的模糊版本。

常规影象与渐进影象之间的载入效果差异

大家也可以考虑使用通用型 CDN 或者影象专用 CDN,其通常会直接提供与影象相关的优化功能。

资源提示 资源提示(Resource hints) 允许我们优化资源交付、降低往返次数,同时获取资源以实现页面浏览过程中的内容交付提速。

带有 link 标签的资源提示

Preload 会在当前页面实际使用之前,通过后台预先下载高优先级资源。

Prefetch 的功能与 preload 类似,用于抓取资源并进行快取,但仅供使用者后续导航使用(低优先级)。

Preconnect 允许 HTTP 请求被实际传送至服务器之前即设定预连线。

提前进行预连线以避免 DNS、TCP 以及 TLS 往返延迟

当然,prerender 以及 dns-prefetch 等其它一些资源提示同样非常重要。其中一部分资源提示可在响应标头中进行指定。需要提醒大家的是,请务必小心使用资源提示。一旦开始滥用,您的页面中可能包含大量不必要的请求并快速下载过量资料,这种情况显然不利于使用蜂窝资料的移动使用者。

总结 应用程序的效能改善之路代表着一个永远尽头的过程,且通常要求我们在整个堆叠当中持续作出更改。

每次看到下面这段视讯,我总会想起你们努力减少应用包大小的样子。

——我的同事

马上把一切不需要的东西从飞机上扔下去! ——电影《珍珠港》

以下列出了我们已经使用或者计划尝试的其它一些潜在效能改进思路:

使用 Service Workers 进行快取、离线支援以及主执行绪分摊。

通过关键 CSS 内联或者函式式 CSS 实现资料包的长效“瘦身”。

使用 WOFF2 字型替代 WOFF 字型(仅举一例,字型变更最高可带来 50% 压缩效果)。

确保 browserslist 的定期更新。

利用 webpack-bundle-analyzer 直观分析构建块。

优选较小的工具包(例如 date-fns)及外挂(例如 lodash-webpack-plugin),从而缩小页面体积。

尝试使用 preact、lit-html 或者 svelte。

在 CI 中执行 Lighthouse。

渐进式 hydration 与 React 流式设计。

另外还有更多令人兴奋的想法可供尝试。希望本文提出的资讯及以下案例研究能够激发出大家改善应用程序效能的更多灵感:

根据亚马逊方面的计算,单一页面 1 秒的响应延时每年可能造成 16 亿美元损失。连结地址: https://www.fastcompany.com/1825005/how-one-second-could-cost-amazon-16-billion-sales

沃尔玛每缩短 1 秒载入时长,即可提升 2% 的客户转换率。每 100 毫秒的提升则可带来 1% 的收入增长。连结地址: https://wpostats.com/2015/11/04/walmart-revenue.html

Google公司计算出,如果搜寻结果显示速度减缓 0.4 秒,则每天搜寻量将减少 800 万次。连结地址: https://www.fastcompany.com/1825005/how-one-second-could-cost-amazon-16-billion-sales

品趣志的页面重构将等待时长缩短了 40%,SEO 流量增加了 15%,注册转换率亦提升 15%。连结地址: https://medium.com/@Pinterest_Engineering/driving-user-growth-with-performance-improvements-cfc50dafadd7

BBC 通过观察发现,网站载入时长每增加 1 秒钟,就会失去 10% 的使用者。连结地址: https://www.creativebloq.com/features/how-the-bbc-builds-websites-that-scale

FT.com 通过测试证明,更快的响应速度令使用者的参与度提高了 30%——这意味着更多的访问次数与更大的内容消费总量。连结地址: https://www.wsj.com/articles/financial-times-hopes-speedy-new-website-will-boost-subscribers-1475553602

Instagram 通过降低显示评论内容所需的 JSON 响应包的大小,成功将展示次数与使用者个人资料滚动操作量增加了 33%。连结地址: https://instagram-engineering.com/performance-usage-at-instagram-d2ba0347e442

英文原文: https://engineering.universe.com/improving-browser-performance-10x-f9551927dcff?gi=ef65642ac481

活动推荐 上半年大前端技术干货集合,GMTC 2019 全球大前端技术大会将于 6 月 20-23 日在北京国际会议中心召开。学习热门前端技术,听这一场大会就够了,部分精彩议题如下:

【腾讯】从程式码保护出发详谈业务安全前端对抗

【阿里】基于 Serverless 的淘宝前端研发模式升级

【美团】基于跨平台框架 Flutter 的动态化平台建设

【京东】京东购物小程式工程化之路

【百度】基于精准测试及 AI 技术的前端质量保证实践

目前大会报名截止倒计时最后 8 天!欢迎点选“阅读原文”和扫描图中二维码了解详细日程,购票咨询:18514549229(同微信)

点个在看少个 bug 2019-12-29 14:51:00

相关文章