这篇文章介绍了一些使前端应用程序加载更快并提供良好用户体验的技术。我们会研究前端的整体架构,如何先加载需要的资源,最大化资源缓存的概率。关于后端如何传递资源,您的页面是否需要成为客户端应用程序,或者如何优化您的应用程序的渲染时间,我不会多说。概述我将把应用程序加载分为三个不同的阶段:初始渲染——用户看到任何东西需要多长时间?应用程序加载-用户可以使用应用程序需要多长时间?下一页——导航到下一页需要多长时间?初始呈现在浏览器的初始呈现之前,用户看不到任何内容。渲染页面至少需要加载HTML文件,但大多数时候还需要加载其他资源,例如CSS和JavaScript文件。一旦全部加载完毕,浏览器就可以开始在屏幕上呈现。在本文中,我将使用WebPageTest瀑布图。您网站的请求瀑布可能看起来像这样。HTML文档将加载一堆其他文件并在加载这些文件后呈现页面。请注意,CSS文件是并行加载的,因此每个额外的请求都不会增加明显的延迟。(备注:gov.uk启用了HTTP/2,因此资产域可以重用现有连接到www.gov.uk!我将在下面更详细地讨论服务器连接。)更少的呈现阻塞请求css和(默认情况下)脚本文件会阻止呈现其下方的任何内容。您可以通过以下几种方式解决此问题:将script标签放在body标签的底部使用async异步加载脚本使用小的JS或CSS片段内联(如果需要同步加载)避免顺序渲染不一定是请求的数量阻止呈现会减慢您的网站速度。更重要的是每个资源的下载大小,以及浏览器需要多长时间才能发现它需要加载资源。如果浏览器在另一个请求完成后才发现需要加载一个文件,它可以获得一个同步请求链。发生这种情况的原因有很多:CSSWebfonts中的@import规则CSS文件中引用的JavaScript注入链接或脚本标签看看这个例子:该网站在其CSS文件之一中使用@import来加载Google字体。这意味着浏览器需要一个接一个地发出这些请求:文件HTML应用程序CSS谷歌字体CSS谷歌字体Woff文件(瀑布图中未显示)要解决此问题,您首先需要更改来自@的谷歌字体CSS请求import移动到HTML中的链接标记,这会断开请求链中的链接。为了进一步加快速度,建议直接在HTML或CSS文件中内联Google字体CSS文件。(请记住,GoogleFonts的CSS响应取决于用户代理。如果您使用IE8发出请求,CSS将引用EOT文件,IE11将获得woff文件,而现代浏览器将获得woff2文件。但是如果您不要介意使用系统字体的旧浏览器,然后您可以复制粘贴CSS文件的内容。)即使在页面开始呈现后,用户可能仍然无法对页面执行任何操作,因为直到字体已加载。这可以通过字体显示交换来避免,谷歌字体现在默认支持。有时消除请求链是不可行的。在这些情况下,请考虑使用预加载或预连接标志。例如,上面的网站可以在发出实际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=>{caches.open("v1").then(function(cache){returncache.addAll(["/app","/app.css"]);});});self.addEventListener("fetch",event=>{event.respondWith(caches.match(event.request).then(cachedResponse=>{returncachedResponse||fetch(event.request);}));});应用程序已加载,现在用户可以看到一些东西,他们还需要什么才能使用您的应用程序?加载应用程序代码(JS和CSS)加载页面的基本数据加载其他数据和图像请注意,延迟渲染不仅仅是延迟从网络加载数据。加载代码后,浏览器需要对其进行解析、编译和执行。Bundlesplit:仅加载必要的代码并最大化缓存命中率Bundlesplit允许仅加载当前页面所需的代码,而不是加载整个应用程序。Bundlesplit也意味着它的一部分可以被缓存,即使其他部分已经改变并且需要重新加载。通常,代码被分成三种不同类型的文件:特定于网页本身的代码共享应用程序代码很少更改的第三方模块(非常适合缓存!)Webpack可以使用optimization.splitChunks自动拆分共享代码以减少总下载量。确保启用运行时分块,以便块哈希稳定并从长期缓存中受益。分离特定于页面的代码无法自动完成,您需要识别可以单独加载的位。通常这是一个特定的路径或一组页面。使用动态导入来延迟加载代码。捆绑拆分会导致发出更多请求来加载您的应用程序。但只要请求是并行发送的,这就不是什么大问题,特别是如果您的网站启用了HTTP/2。您可以在该瀑布中看到前三个请求:但是,该瀑布还显示了两个按顺序发出的请求。这些块仅在此页面中需要,并通过import()调用动态加载。如果你知道你需要这些块,你可以通过插入预加载链接标签来解决这个问题。但是,正如您将看到的,与页面总加载时间相比,这样做的好处可能很小。此外,使用预加载有时会适得其反,因为加载其他更重要的文件可能会出现延迟。加载页面数据您的应用程序可能用于显示一些数据。这里有一些提示,您可以使用它们来尽早加载数据并避免渲染延迟。在开始加载数据之前不要等待bundle这是顺序请求链的一个特例:您加载应用程序bundle,然后代码请求页面数据。有两种方法可以避免这种情况:在HTML文档中嵌入页面数据通过文档中的内联脚本发起数据请求在HTML中嵌入数据可确保您的应用程序不必等待数据加载。这也降低了应用程序的复杂性,因为您不必处理加载状态。但是,如果获取数据会显着延迟您的文档响应,那将不是一个好主意,因为这会延迟您的初始渲染。在这种情况下,或者如果您通过serviceworker提供缓存的HTML文档,您可以将内联脚本嵌入到HTML中以加载此数据。您可以将其作为全局承诺提供,如下所示:window.userDataPromise=fetch("/me")然后,如果数据准备就绪,您的应用程序可以立即开始呈现,或等到它准备好。对于这两种技术,您需要知道在应用开始呈现之前页面必须加载哪些数据。对于与用户相关的数据(用户名、通知...),这往往很容易,但对于特定于页面的内容,则比较棘手。考虑确定最重要的页面并为这些页面编写自定义逻辑。不要在等待非必要数据时阻塞渲染有时生成页面数据需要缓慢复杂的后端逻辑。在这些情况下,如果数据的简单版本足以使您的应用程序正常运行和交互,则可以先加载该数据的简单版本。例如,分析工具可以在加载图表数据之前先加载所有图表的列表。这使用户可以立即找到他们感兴趣的图表,还有助于将后端请求分散到不同的服务器上。避免顺序数据请求链这可能与我之前关于在第二个请求中加载非必要数据的观点冲突,但如果每个完成的请求不会导致向用户显示更多信息,请避免顺序请求链。与其请求用户登录身份,然后请求他们所属的团队列表,不如返回团队列表以及用户信息。您可以使用GraphQL,但是自定义用户呢?includeTeams=true端点也很有用。服务器端渲染服务器端渲染意味着在服务器上预渲染您的应用程序并以整页响应文档请求,而不是首先询问用户以谁身份登录,然后询问他们所属的团队列表HTML。这意味着客户端无需等待其他代码或数据加载即可看到完全呈现的页面!由于服务器只是向客户端发送静态HTML,您的应用还不能进行交互。应用程序需要加载,需要重新运行渲染逻辑,然后将必要的事件监听器附加到DOM。如果您看到非交互式内容的价值,请使用服务器渲染。如果您可以在服务器上缓存呈现的HTML并将其提供给所有用户而不延迟初始文档请求,这也会有所帮助。例如,如果您使用React来呈现博客文章,服务器呈现是完美的。下一页在某个时候,用户将与您的应用程序交互并转到下一页。打开初始页面后,您可以控制浏览器中发生的事情,以便为下一次交互做好准备。预取资源如果预加载下一页所需的代码,则可以消除用户启动导航时的延迟。使用prefetch标签或webpackPrefetch进行动态导入:import(/*webpackPrefetch:true,webpackChunkName:"todo-list"*/"./TodoList")请注意您使用了多少用户数据和带宽,尤其是当他们使用移动设备时连接时。如果他们使用您网站的移动版本,或者如果他们启用了省流量模式,您可以减少预加载。为用户最可能需要的应用程序部分制定策略。重用已加载的数据在应用程序本地缓存Ajax数据并使用它来避免将来的请求。如果用户从Teams列表导航到EditTeam页面,您可以通过重用已获取的数据立即进行转换。请注意,如果您的实体经常被其他用户编辑并且您下载的数据可能已过时,这将不起作用。在这些情况下,首先考虑在获取最新数据时以只读方式显示现有数据。结论本文涵盖了在加载过程中的不同点可能会降低页面速度的多种因素。使用ChromeDevTools、WebPageTest和Lighthouse等工具来确定哪些适用于您的应用程序。事实上,您几乎不可能优化所有内容。找出对您的用户影响最大的因素,然后专注于此。在撰写本文时我意识到的一件事是,我深信发出许多单独的请求对性能不利。这曾经是每个请求都需要一个单独的连接,而浏览器只允许每个域几个连接的情况。但是,对于HTTP/2和现代浏览器,情况已不再如此。并且有充分的理由支持拆分请求。它只允许加载必要的资源,并且可以更好地利用缓存的内容,因为只有更改的文件才需要重新加载。
