智能优化技术:使用React进行懒加载(lazy loading)

1,396 阅读7分钟

如果您曾经使用过中型到大型Web应用程序,那么您很有可能会注意到捆绑包的大小(发送到浏览器的JavaScript代码的数量)是一个不断增长的怪兽。使用所有添加的库或仅管理员功能,对于普通用户来说,您的应用程序开始加载的速度就慢了一点。虽然它仍可能会感到高性能在你的笔记本电脑,人与恶劣的接收的地方,去年的手机型号要迅速感觉到性能下降。

事实证明,主要问题实际上并不是您下载的总代码量,而是您向用户展示任何感兴趣内容之前必须下载(解析和运行)的代码量。对于某些用户来说,这种延迟可能真的很长,并且肯定会转化为您的业务损失美元。换句话说-这是值得投资的挑战。

本文的目的是给你一个伟大的介绍延迟加载作为一个概念,它是如何在做了反应,有什么方法使用和 时,你会得到最好的结果可能。

什么是懒加载(延迟加载)?

延迟加载是指仅在需要时才需要获取代码的技术。您将大型JavaScript包拆分为一堆不同的部分,这是一种称为代码拆分的技术,该技术可以包含页面,部分甚至单个组件的代码。当需要该代码时,您可以向服务器请求该特定代码,然后将其下载并加载到内存中-就像它一直都在那儿一样。

将您的应用分成几个部分会带来一系列挑战。确定要放入每个捆绑软件中的内容,何时下载它以及在等待代码下载时该做什么是我们需要解决的所有挑战。

React中延迟加载的基础

由于这种代码拆分非常重要,因此React附带了对处理的内置支持。让我们从一个小例子开始:

import { Router, Route } from 'react-router-dom';
import LandingPage from './pages/landing-page';

const AdminPage = React.lazy(() => import('./pages/admin-page'));

export const App = () => {
  return (
    <React.Suspense fallback={<Spinner />}>
      <Router>
        <Route path="/" component={LandingPage} />
        <Route path="/admin" component={AdminPage} />
      </Router>
    </React.Suspense>
  );
}

我们正在创建一个包含两条路线的应用-/和/admin。默认情况下,我们会将登录页面与应用程序的主捆绑包捆绑在一起(并像往常一样加载)。但是,在管理页面中,我们正在做一些不同的事情。

这条线React.lazy(() => import('./pages/admin-page'))可能会作为许多人的新语法出现-但是它已经内置到JavaScript中已有几年了。让我们快速浏览一下正在发生的事情,以及这对用户体验的意义。

该 import('./some/file')语法称为动态导入,它们通过请求捆绑软件的特定部分来工作。它们通常在诸如Webpack或Parcel的捆绑工具中用作标记,并使这些工具输出更多的JavaScript文件,且每个文件中的代码更少。

该React.lazy()函数基本上告诉React在我们等待应用程序的这一特定部分从服务器加载并被解析时“暂停”其呈现。挂起的概念非常新颖,但是现在您需要知道的是,<React.Suspense />每当我们等待代码下载时,包裹我们的应用程序的组件将显示一个后备UI。

换句话说-流程如下:

image.png

探索演示

这听起来似乎很合理,但是让我们看一个演示以在实践中看到它。

在CodeSandbox中,我们正在延迟加载使用一些非常大的库的组件。由于我们仅在有问题的组件中使用这些库,因此我们可以推迟加载它们,直到我们希望在屏幕上显示它们为止。

渲染“冗余”组件时,我们将其包装在<React.Suspense />边界中。这意味着我们在等待组件代码下载时可以显示后备UI(例如微调器,框架UI或只是文本字符串)。

在Chrome开发工具中,此过程的外观如下:

image.png

命名捆绑,加载顺序调整

我们加载的捆绑软件可以正常工作,但是它们的名称并不恰当。0.chunk.js并且1.chunk.js可能把工作做好,但它不是非常适合在生产调试错误。

幸运的是,有一种方法可以改善块命名。Webpack支持一种称为“魔术注释”的特殊注释语法,该语法使您可以命名不同的块。

首先,让我们进入App.tsx文件,在其中指定动态导入。我们需要更改此:

  () => import("./SomeExpensiveComponent")
);

对此:

  () =>
    import(
      /* webpackChunkName: "SomeExpensiveComponent" */ "./SomeExpensiveComponent"
    )
);

现在,我们可以重新加载页面,并注意到块名称已更改!

image.png

代码拆分及加载策略

代码拆分和延迟加载代码非常适合仅加载用户当前正在使用的应用程序部分。但是,您应在何处设置应捆绑在一起的内容以及应延迟加载的内容的阈值?

有两种主要方法,它们都有优点和缺点。

基于路由的拆分

许多开发人员首先根据路由拆分他们的应用程序,以便每个路由(或应用程序中的屏幕)都具有自己的捆绑包。对于您有很多路线但用户很少访问全部路线的情况,这是一种很好的方法。这个概念也很有意义-为什么要下载用户尚未访问的页面的代码?

基于路由拆分应用程序的缺点是,您失去了浏览单个页面应用程序的即时感觉。除了立即浏览之外,我们还需要在后台将新页面显示给用户之前下载它。可以通过多种技术来减轻这种情况(例如,每当用户悬停链接时就预加载页面),但是很难正确地做到这一点。

基于组件的拆分

一些开发人员决定将大多数主要组件拆分为自己的捆绑包。由于路线本身是捆绑在一起的,因此这可以通过基于路线的方法解决大多数挑战。

但是,这种方法也有缺点。每次我们懒惰地加载组件时,我们都必须在其后台加载时提供一个后备UI。而且,每个块都带有一点点开销和网络延迟,与不做任何事情相比,它们积累的体验甚至更慢。

所有基于拆分的事情 我的经验是,通过结合两种方法,并在任何给定情况下考虑哪种方法最物有所值,我将获得最佳结果。

一个很好的例子是将您的应用程序分为“已登录的应用程序”和“已注销的应用程序”的想法(由Kent C. Dodds关于React中的身份验证的文章提供)。在这里,您将在应用程序的两个部分之间进行高层划分,而这两个部分很少会重叠。

另一个例子是代码分离出很少使用的路由,例如联系页面或隐私策略。在这种情况下,显示微调框一秒钟可能是完全可以接受的,因为这不是主要用户体验的一部分。

第三个示例是在某些情况下您必须加载特别大的组件-例如日期选择器库,用于当用户必须在注册流程中输入其生日时。由于大多数用户都不需要它-我们只是显示一个备用用户界面,而我们会懒惰地为那些需要的用户加载额外的代码。

考虑每种情况,并以字节为单位衡量它提供的差异。许多构建系统(例如create-react-app)为您提供与上次构建有所不同的包大小和单个块大小。不要害怕尝试-但要确保性能上的差异值得较高复杂性的权衡。

总结语

对于大多数现代Web应用程序来说,添加一些延迟加载可能是一件好事。您仅在需要时才向最终用户交付较少的代码。入门非常容易,而且只要您考虑针对特定用例使用哪种策略,也很容易将其扩展到整个应用程序。