复杂的前端。快速站点的正确架构

哈Ha!



长期以来,我们一直忽略了浏览器,CSS和可访问性这一主题,并决定将其与今天的概述材料(原始版本-2020年2月)一起翻译。我对您对这里提到的服务器渲染技术以及对一本关于HTTP / 2的完整书籍的迫切需求的看法特别感兴趣,但是,让我们按顺序进行讨论。



这篇文章介绍了一些技术,可以加快前端应用程序的加载速度,从而提高可用性。



让我们看一下前端的一般体系结构。您如何确保关键资产首先加载,并最大程度地提高这些资产已经在缓存中结束的可能性?



无论您是需要以客户端应用程序的形式制作页面还是如何优化应用程序的渲染时间,我都不会讨论后端如何提供资源。



总览



让我们将应用下载过程分为三个单独的步骤:



  1. 主要渲染-用户看到内容之前需要多长时间?
  2. 应用下载-用户看到该应用需要多长时间?
  3. – ?




直到主要渲染(rendering)阶段,用户根本看不到任何东西。要呈现页面,您至少需要一个HTML文档,但是在大多数情况下,您还需要加载其他资源,例如CSS和JavaScript文件。如果可用,浏览器可以开始渲染到屏幕上。



在本文中,我将使用WebPageTest瀑布您网站的级联请求看起来像这样。







一堆其他文件与HTML文档一起加载,并且所有这些文件都在RAM中后才显示该页面。请注意,CSS文件是并行加载的,因此每个后续请求不会显着增加延迟。



减少渲染阻止请求的数量



样式表和(默认情况下)脚本元素不允许显示其下方的任何内容。



有几种方法可以解决此问题:



  • 我们将脚本标签放在标签的最底部 body
  • 使用异步加载脚本 async
  • 如果要同步加载,则内联编写小块JS或CSS


避免呈现阻塞查询链



导致网站速度下降的不仅是阻止渲染的请求数量。重要的是每个需要下载的资源的大小,以及确切的浏览器何时检测到需要下载的资源。



如果浏览器仅在另一个请求完成后才意识到需要下载文件,那么您可能会遇到一系列同步请求。发生这种情况有几个原因:



  • @import在CSS中有规则
  • 使用CSS文件中引用的网络字体
  • JavaScript注入链接或脚本标签


考虑以下示例:







该站点上的一个CSS文件包含一个@import加载Google字体的规则因此,浏览器必须按此顺序一个接一个地执行以下请求:



  1. 文件HTML
  2. 应用程式CSS
  3. Google字体CSS
  4. Google Font Woff文件(未在层叠中显示)


要解决此问题,我们首先将对Google Fonts CSS的请求从@import移至HTML文档中的link标记。这样会将链缩短一个链接。



为了进一步提高速度,请将Google字体CSS文件直接嵌入HTML或CSS文件中。



(请记住,来自Google Fonts的CSS响应取决于用户代理。如果您使用IE8进行请求,则CSS将引用EOT文件(由OpenType嵌入),IE11将收到woff文件,现代浏览器将收到woff2。但是,如果您对此感到满意与使用系统字体的相对较旧的浏览器一样,您只需复制并粘贴CSS文件的内容即可。)



即使页面开始呈现之后,用户也可能无法对其进行任何操作,因为在完全加载字体之前不会显示任何文本。可以通过使用font-display swap属性来避免这种情况,该属性现在是Google字体中的默认属性



有时您无法完全摆脱请求链。在这种情况下,请尝试使用preload标签preconnect例如,上面显示的站点可能fonts.googleapis.com在发出实际CSS请求之前已连接到该站点



重用服务器连接以加快请求



通常,建立新的服务器连接需要在浏览器和服务器之间进行3次往返传递:



  1. DNS查询
  2. 建立TCP连接
  3. 建立SSL连接


建立连接后,至少需要再进行一次往返:发送请求并下载响应。



如下面的级联所示,将启动到四个不同服务器的连接:hostgator.com,optimize.com,googletagmanager.com和googelapis.com。



但是,对受影响服务器的后续请求可以重用现有连接。因此,base.cssindex1.css迅速加载,因为它们也位于hostgator.com。







减小文件大小并使用内容传送网络(CDN)



您控制的其他两个因素会影响请求的持续时间以及文件大小:资源大小和服务器位置。



向用户发送所需的最少数据量,此外,还要注意他们的压缩(例如,使用brotli或gzip)。



内容分发网络(CDN)在各种各样的位置提供服务器,因此很可能其中之一将位于您的用户附近。您可以将它们连接到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);
   })
 );
});


有关使用Service Worker预加载和缓存资源的更多信息,请参阅本教程



应用下载



好的,我们的用户已经看到了一些东西。他还需要什么才能使用我们的应用程序?



  1. 加载应用程序(JS和CSS)
  2. 加载页面的最重要数据
  3. 下载其他数据和图像






请注意,不仅通过网络加载数据还会降低渲染速度。加载代码后,浏览器将需要解析,编译和执行它。



拆分捆绑包:仅加载必要的代码并最大化缓存命中率。



通过拆分捆绑软件,您可以仅下载仅此页面所需的代码,而不下载整个应用程序。拆分包时,即使代码的其他部分已更改并且需要重新加载,也可以将其部分缓存。



通常,代码由三种不同类型的文件组成:



  • 特定于此页面的代码
  • 共享应用程序代码
  • 很少更改的第三方模块(非常适合缓存!)


Webpack可以自动拆分拆分的代码,以减少下载的总重量,这是使用optimization.splitChunks完成的。确保启用运行时块,以便块的哈希保持稳定,并且可以有效地应用长期缓存。 Ivan Akulov已编写了有关共享和缓存Webpack代码的综合指南



页面特定代码的拆分无法自动完成,因此您必须确定可以单独加载的代码段。这通常是一条特定的路线或一组页面。使用动态导入来延迟加载此类代码。



拆分捆绑软件将导致发出更多请求以完全加载您的应用程序。但是,如果将请求并行化,则这个问题并不大,尤其是在使用HTTP / 2的网站上。请注意此级联中的前三个查询:







但是,此级联还显示了按顺序执行的2个查询。这些片段仅在此页面上需要,并使用call动态加载import()如果您肯定需要这些片段,则



可以通过插入标签来解决preload link







但是,如您所见,在这种情况下,与总页面加载时间相比,速度的提高可能很小。



此外,使用预加载有时会适得其反,并且在加载其他更重要的文件时可能会导致延迟。查看Andy Davis关于字体预加载的文章,以及如何通过先加载字体然后阻止加载渲染的CSS来阻止主要渲染。



载入页面数据



可能是您的应用程序旨在显示某种数据。以下是一些有关如何提前加载数据并避免渲染延迟的提示。



不要等待捆绑包,立即开始加载数据,



这可能是一系列顺序请求的特殊情况:加载应用程序捆绑包,并且此代码已经请求页面数据。



有两种方法可以避免这种情况:



  1. 将页面数据嵌入HTML文档
  2. 通过位于文档内部的内联脚本开始请求数据


在HTML中嵌入数据可确保您的应用程序不必等待加载。通过不必处理加载状态,它还降低了应用程序的整体复杂性。



但是,如果获取数据会导致文档响应的显着延迟,则此想法不是很好,因为这也会减慢初始呈现的速度。



在这种情况下,或者当使用服务工作者提供缓存的HTML文档时,您可以在HTML中嵌入一个内联脚本以加载此数据。可以将其作为全球承诺提供,如下所示:



window.userDataPromise = fetch("/me")


然后,如果数据已经准备好,则您的应用程序可以立即开始渲染,或者等待直到准备就绪。



使用这两种方法时,甚至在应用程序开始呈现之前,您都需要确切地知道应该在页面上显示哪些数据。通常,对于用户特定的数据(名称,通知...)来说,这很容易提供,但是在处理页面特定的内容时,这并不容易。尝试自己突出显示最重要的页面,并为每个页面编写自己的逻辑。



等待不相关的数据时不要阻塞渲染



有时生成分页数据需要在后端实现缓慢而复杂的逻辑。在这种情况下,首先要加载简化版本的数据,这足以使您的应用程序具有功能性和交互性。



例如,分析工具可能会首先加载所有图表,然后将它们与数据一起显示。因此,用户将能够立即查看他感兴趣的图表,并且您将有时间在不同的服务器之间分配后端请求。







避免顺序数据查询链



该建议似乎与我之前提到的有关将不相关数据的加载推迟到第二个请求的观点相矛盾。但是,如果链中的后续请求未向用户提供任何新信息,则避免链接顺序请求。



返回第一个组列表以及有关该用户的信息,而不是先询问用户登录的内容,然后再询问该用户所属的组的列表。您可以为此使用GraphQL,但是自定义端点也user?includeTeams=true可以。



服务器端渲染



在这种情况下,我们指的是在服务器上预先渲染应用程序,以便将完整的HTML页面用作对文档请求的响应。因此,客户端可以看到整个页面,而无需等待其他代码或数据被加载!



由于服务器仅向客户端发送静态HTML,因此您的应用程序在此阶段仍然没有交互性。需要加载应用程序,它需要重新运行渲染逻辑,然后将所需的事件侦听器附加到DOM。



如果发现非交互式内容本身很有价值,请使用服务器端呈现。同样,这种方法有助于缓存服务器上显示的HTML,然后在首次请求文档时立即将其传输给所有用户。例如,如果您使用React渲染博客,那么服务器端渲染就很棒。



阅读Michal Janaszek的这篇文章它很好地描述了如何将服务工作者与服务器端渲染结合起来。



下一页



在某些时候,使用您的应用程序的用户将需要转到下一页。打开第一页后,您可以控制浏览器中发生的所有事情,因此可以为下一次交互做准备。



预取资源



预取显示下一页所需的代码可以帮助避免自定义导航中的延迟。使用标签prefetch linkwebpackPrefetch动态导入:



import(
    /* webpackPrefetch: true, webpackChunkName: "todo-list" */ "./TodoList"
)


考虑您正在使用多少用户数据以及带宽是多少,尤其是在移动连接方面。在网站的移动版本中,您不能热衷于预加载,也不能激活数据保存模式。



从战略上选择用户最需要的数据。



重用已经加载



的数据在应用程序中本地缓存Ajax数据,以避免以后不必要的请求。如果用户导航到“编辑组”页面上的组列表,则可以通过重新使用之前已经选择的数据来立即进行转换。



请注意,如果您的对象被其他用户频繁编辑,并且您上载的数据很快就会过时,则此方法将无效。在这种情况下,请尝试首先以只读模式显示现有数据,同时选择更新的数据。



结论



在本文中,我们研究了许多因素,这些因素可能会导致加载过程中各个点的页面速度变慢。使用诸如Chrome DevToolsWebPageTestLighthouse之类的工具来确定哪些技巧与您的应用程序相关。



实际上,几乎不可能进行全面优化。确定什么对您的用户最重要,并专注于此。



在撰写本文时,我意识到自己有着根深蒂固的信念,即多个查询都是性能不好的问题。过去确实如此,当时每个请求都需要一个单独的连接,而浏览器每个域只允许几个连接。但是随着HTTP / 2和现代浏览器的出现,这个问题消失了。



拆分查询有很强的论据。这样,您可以加载严格必要的资源并更好地利用缓存的内容,因为您只需要重新加载已更改的文件。



All Articles