这篇文章介绍了一些使前端应用程序加载更快并提供良好用户体验的技术。
我们将研究前端的总体架构。您如何首先加载必需的资源,并最大化资源已在缓存中的概率?
无论您的页面甚至需要是客户端应用程序,还是如何优化应用程序的呈现时间,我都不会说太多。
总览
我将把应用程序加载分为三个不同的阶段:
- 初始渲染–用户看到任何东西需要多长时间?
- 应用程序加载–用户可以使用该应用程序多长时间?
- 下一页–导航到下一页需要多长时间?
初始渲染
在浏览器的初始渲染之前,用户看不到任何东西。渲染页面至少需要加载HTML文档,但是大多数时候需要加载其他资源,例如CSS和JavaScript文件。一旦这些可用,浏览器就可以开始在屏幕上绘画。
在本文中,我将使用WebPageTest瀑布图。您网站的请求瀑布可能看起来像这样。
HTML文档将加载一堆其他文件,并在这些文件加载后呈现页面。请注意,CSS文件是并行加载的,因此每个其他请求不会增加明显的延迟。
(旁注:gov.uk 现在已启用HTTP / 2,因此资产域可以重新使用与www.gov.uk的现有连接!我将在下面详细讨论服务器连接。)
减少渲染阻止请求
样式表和(默认情况下)脚本元素会阻止其下方的任何内容呈现。
您可以通过以下几种方法来解决此问题:
- 将脚本标签放在body标签的底部
- 与异步加载脚本
async
- 内联小型JS或CSS代码段(如果需要同步加载)
避免顺序渲染阻止请求链
让您的网站变慢的不一定是阻止渲染的请求数量。更重要的是每种资源的下载大小,以及浏览器发现需要加载资源的时间。
如果浏览器仅在另一个请求完成后才发现需要加载文件,则可以获得同步请求链。发生这种情况可能有多种原因:
- CSS中的@import规则
- CSS文件中引用的Webfonts
- JavaScript注入链接或脚本标签
看一下这个例子:
该网站在其CSS文件之一中使用@import加载Google字体。这意味着浏览器需要一个接一个地发出这些请求:
- 文件HTML
- 应用程式CSS
- Google字体CSS
- Google Font Woff文件(在瀑布图中未显示)
要解决此问题,请先将请求从@import移至Google字体CSS,然后将其移至HTML文档中的链接标记。这切断了链条的一个环节。
为了进一步加快速度,请直接在HTML或CSS文件中内联Google字体 CSS文件。
(请注意,来自Google字体的CSS响应取决于用户代理。如果您使用IE8进行请求,则CSS将引用EOT文件,IE11将获得woff文件,现代浏览器将获得woff2文件。但是如果如果您对使用系统字体的旧版浏览器没问题,则可以复制并粘贴CSS文件的内容。)
即使页面开始呈现之后,用户仍可能无法对该页面执行任何操作,因为在加载字体之前,不会显示任何文本。可以通过font-display swap来避免这种情况,默认情况下,Google Fonts现在会使用它。
有时,消除请求链是不可行的。在这些情况下,您可以考虑预加载或预连接标签。例如,在提出实际的CSS请求之前,上述网站可以连接到fonts.googleapis.com。
重复使用服务器连接以加快请求
建立新的服务器连接通常需要在服务器的浏览器之间进行3次往返:
- DNS查询
- 建立TCP连接
- 建立SSL连接
连接就绪后,至少需要再进行一次往返行程,才能发送请求并下载响应。
下面的瀑布显示连接已启动到四个不同的服务器:hostgator.com,optimize.com,googletagmanager.com和googelapis.com。
但是,对同一服务器的后续请求可以重新使用现有连接。因此,加载base.css或index1.css的速度很快,因为它们也托管在hostgator.com上。
减小文件大小并使用CDN
除了文件大小以外,还有两个其他因素会影响请求时间,这些因素都在您的控制范围内:资源大小和服务器位置。
向用户发送尽可能少的数据,并确保其已压缩(例如,使用brotli或gzip)。
内容交付网络在大量位置提供服务器,因此其中之一可能位于您的用户附近。用户可以连接到附近的CDN服务器,而不必连接到中央应用程序服务器。这意味着服务器往返时间将大大缩短。这对于诸如CSS,JavaScript和图像之类的静态资产特别方便,因为它们易于分发。
与服务人员跳过网络
服务人员可以让您在请求进入网络之前对其进行拦截。这意味着您可以获得几乎即时的第一道油漆!
当然,这仅在不需要网络发送响应的情况下才有效。您需要已经缓存了响应,因此用户只会在第二次加载您的应用程序时受益。
下面的服务人员缓存呈现页面所需的HTML和CSS。再次加载该应用程序时,它将尝试提供缓存的资源,如果不可用,则会退回到网络。
self.addEventListener("install", async e => {
caches.open("v1").then(function (cache) {
return cache.addAll(["/app", "/app.css"]);
});
});
self.addEventListener("fetch", event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
return cachedResponse || fetch(event.request);
})
);
});
阅读本指南以了解有关使用服务工作者预加载和缓存资源的更多信息。
应用加载
好的,现在用户可以看到一些东西。他们可以使用您的应用程序之前还需要什么?
- 加载应用程序代码(JS和CSS)
- 加载页面的基本数据
- 加载其他数据和图像
请注意,不仅延迟从网络加载数据,还会延迟渲染。加载代码后,浏览器将需要解析,编译和执行它。
捆绑包拆分:仅加载必要的代码,并最大化缓存命中率
捆绑包拆分允许您仅加载当前页面所需的代码,而不是加载整个应用程序。拆分包也意味着可以缓存其中的一部分,即使其他部分已更改并且需要重新加载也是如此。
通常,代码分为三种不同类型的文件:
- 网页专用代码
- 共享应用程序代码
- 很少更改的第三方模块(非常适合缓存!)
Webpack可以使用optimization.splitChunks自动拆分共享代码以减少总下载量。确保启用运行时块,以使块哈希稳定,并从长期缓存中受益。Ivan Akulov已经编写了有关Webpack代码拆分和缓存的深入指南。
分页特定的代码无法自动完成,您需要确定可以单独加载的位。通常这是一条特定的路线或一组页面。使用动态导入来延迟加载该代码。
捆绑包拆分将导致发出更多请求以加载您的应用程序。但是只要请求是并行发出的,这并不是什么大问题,尤其是如果您的站点通过HTTP / 2服务时。您可以在该瀑布的前三个请求中看到这一点:
但是,该瀑布还显示了2个按顺序提出的请求。这些块仅是此页面所需的,并通过import()调用动态加载。
如果您知道需要这些块,则可以通过插入预加载链接标记来解决此问题。
但是,您会看到,与总页面加载时间相比,这样做的好处可能很小。
另外,使用预加载有时会适得其反,因为加载其他更重要的文件时可能会延迟。查看Andy Davies的有关预加载字体的文章,以及如何通过在阻止渲染的CSS之前加载字体来阻止初始渲染。
载入页面数据
您的应用程序可能存在以显示一些数据。这里有一些技巧,您可以用来尽早加载此数据并避免渲染延迟。
开始加载数据前,请不要等待捆绑包
这是顺序请求链的一种特例:加载应用程序包,然后该代码请求页面数据。
有两种方法可以避免这种情况:
- 将页面数据嵌入HTML文档中
- 通过文档中的内联脚本启动数据请求
将数据嵌入HTML可以确保您的应用程序不必等待数据加载。这也降低了应用程序的复杂性,因为您不必处理加载状态。
但是,如果获取数据会大大延迟您的文档响应,那将不是一个好主意,因为这会延迟您的初始渲染。
在这种情况下,或者如果您通过服务工作者提供缓存的HTML文档,则可以将内联脚本嵌入到HTML中以加载此数据。您可以将其作为全球承诺提供,如下所示:
window.userDataPromise = fetch("/me")
然后,如果数据准备就绪,您的应用程序可以立即开始渲染,或者等到准备就绪。
对于这两种技术,您都需要知道在应用开始呈现之前页面必须加载哪些数据。对于与用户相关的数据(用户名,通知,...),这往往很容易,但是对于特定于页面的内容,则比较棘手。考虑确定最重要的页面并为这些页面编写自定义逻辑。
等待非必需数据时不要阻塞渲染
有时生成页面数据需要缓慢的复杂后端逻辑。在这些情况下,如果足以使您的应用程序具有功能性和交互性,则可以首先加载较简单的数据版本。
例如,分析工具可以在加载图表数据之前首先加载所有图表的列表。这使用户可以立即查找他们感兴趣的图表,还可以帮助将后端请求分散到不同的服务器上。
避免顺序数据请求链
这可能与我先前关于在第二个请求中加载非必要数据的观点相冲突,但是如果每个完成的请求都不会导致向用户显示更多信息,则避免顺序请求链。
与其先发出有关用户登录身份的请求,然后再请求其所属团队的列表,不如返回用户列表以及用户信息。您可以为此使用GraphQL,但是自定义user?includeTeams=true
端点也可以很好地工作。
服务器端渲染
服务器端渲染意味着在服务器上预渲染您的应用程序,并使用整页HTML响应文档请求。这意味着客户端可以看到完整呈现的页面,而不必等待其他代码或数据被加载!
由于服务器只是将静态HTML发送给客户端,因此您的应用尚无法进行交互。需要加载应用程序,它需要重新运行呈现逻辑,然后将必要的事件侦听器附加到DOM。
如果看到非交互式内容很有价值,请使用服务器呈现。如果您能够在服务器上缓存呈现的HTML并将其提供给所有用户,而又不会延迟初始文档请求,那么它也将有所帮助。例如,如果您使用React来渲染博客文章,则服务器渲染非常合适。
阅读MichałJanaszek的这篇文章,以了解如何将服务端与服务器端渲染结合在一起。
下一页
在某个时候,用户将与您的应用进行交互并转到下一页。打开初始页面后,您可以控制浏览器中发生的事情,因此您可以准备进行下一次交互。
预取资源
如果您预加载了下一页所需的代码,则可以消除用户启动导航时的延迟。使用预取链接标记,或webpackPrefetch
用于动态导入:
import(
/* webpackPrefetch: true, webpackChunkName: "todo-list" */ "./TodoList"
)
保留您正在使用的用户数据和带宽量,尤其是在用户使用移动连接时。如果他们使用网站的移动版本,或者启用了保存数据模式,则可以降低预加载的积极性。
对用户最可能需要应用程序的哪些部分制定策略。
重用已经加载的数据
在应用程序本地缓存Ajax数据,并使用它避免将来的请求。如果用户从团队列表导航到“编辑团队”页面,则可以通过重新使用已经获取的数据来进行转换。
请注意,如果您的实体被其他用户频繁编辑,则此方法将无效,并且您下载的数据可能已过期。在这些情况下,请考虑在获取最新数据时首先以只读方式显示现有数据。
结论
本文介绍了许多因素,这些因素可能会在加载过程的不同时刻使您的页面速度减慢。使用Chrome DevTools,WebPageTest和Lighthouse之类的工具来确定其中哪些适用于您的应用程序。
实际上,您几乎不可能在所有方面进行优化。找出对用户有最大影响的因素,并专注于此。
我在写这篇文章时意识到的一件事是,我根深蒂固地相信,发出许多单独的请求对性能不利。过去,当每个请求都需要一个单独的连接时,Thas就是这样,而浏览器每个域只允许几个连接。但是,使用HTTP / 2和现代浏览器已不再是这种情况。
并且有强烈的理由支持拆分请求。它允许仅加载必要的资源,并可以更好地利用缓存的内容,因为仅需要重新加载已更改的文件。
via https://www.debugbear.com/blog/performant-front-end-architecture