浅谈 React-Router 6.4+ 的 Lazy Loading Routes

最近看了一篇介绍 React-Router 6.4 Lazy Loading Routes 的文章,感觉收获很大,在这里分享一下。

首屏性能优化是一个老生常谈的问题,其中最常见的解法就是 Code-Splitting 路由懒加载。其中 import() 语法由 Webpack 等打包工具进行支持(浏览器环境降级为 __webpack_require__.e 通过 JSONP 方式加载异步 chunk),与框架无关,但是与前端框架产生割裂。从 React 17 开始,React 从框架层面提供了 React.lazy()<Suspense /> 原语实现懒加载。但是 React Router 6.4+ 之后,路由懒加载又有新的写法了。

为啥 React Router 本身要提供懒加载功能?这里需要先介绍一下 React Router v6.4 引入的 Data Router 功能。推荐看一下 Ryan Florence(Remix 和 React Router 作者)的演讲:When To Fetch: Remixing React Router

Ryan 核心观点是假如路由和 data fetch 逻辑解耦,那么组件渲染、接口请求就是串行的,进而导致瀑布流问题:

如果引入 Data Router,路由整合了部分数据流功能,组件渲染、接口请求就可以并行,提升页面加载性能:

具体实现通过 data router 将接口请求逻辑提升到路由层面:

tsx 复制代码
// app.jsx
import Layout, { getUser } from `./layout`;
import Home from `./home`;
import Projects, { getProjects } from `./projects`;
import Project, { getProject } from `./project`;

const routes = [{
  path: '/',
  loader: () => getUser(),
  element: <Layout />,
  children: [{
    index: true,
    element: <Home />,
  }, {
    path: 'projects',
    loader: () => getProjects(),
    element: <Projects />,
    children: [{
      path: ':projectId',
      loader: ({ params }) => getProject(params.projectId),
      element: <Project />,
    }],
  }],
}]

但是以上代码会存在一个问题,data router 必须等待所有 Initial Chunk 加载完成才会发起请求,即首屏加载不需要的资源会阻塞 data fetch:

解法大家都知道,就是通过 Code-Splitting。这里为啥不建议用 React 提供的 React.lazy(),因为该方式只会在真正渲染的时候再加载异步组件,无法提前加载,特别是嵌套路由比较明显,会导致瀑布流问题(例如先下载 <App /> 组件代码、渲染 <App /> 组件、再下载 <Component /> 组件代码、渲染 <Component />组件)。

这里可以借鉴上面 data loader,既然数据可以通过 loader 方式提升到路由层面并行加载,异步 chunk 加载也可以提升到路由层面进行 prefetch 预加载。

React Router 路由配置,可以分为三个部分:

  • 路径匹配字段:pathindexchildren
  • 数据加载、提交相关字段:loaderaction
  • 渲染相关字段:elementerrorElement

实际上对路由来说,最关键的就是路径匹配字段,其他逻辑都可以异步加载。另外,data fetch 逻辑和组件实际上是捆绑的关系,需要渲染组件的时候,才会调相关接口。这个意义上,路由配置可以优化如下:

tsx 复制代码
// app.jsx
import Layout, { getUser } from `./layout`;
import Home from `./home`;

const routes = [{
  path: '/',
  loader: () => getUser(),
  element: <Layout />,
  children: [{
    index: true,
    element: <Home />,
  }, {
    path: 'projects',
    lazy: () => import("./projects"), // 💤 Lazy load!
    children: [{
      path: ':projectId',
      lazy: () => import("./project"), // 💤 Lazy load!
    }],
  }],
}]

// projects.jsx
export function loader = () => { ... }; // formerly named getProjects

export function Component() { ... } // formerly named Projects

// project.jsx
export function loader = () => { ... }; // formerly named getProject

export function Component() { ... } // formerly named Project

这样首屏加载效率可以明显提升:

但是需要注意,渲染子路由的时候,由于 loader 和组件打包到同一个异步 chunk,因此接口请求需要等待 chunk 加载完成,如果组件的逻辑比较多,仍会阻塞 data fetch:

如何优化上面的问题,解法是将 loader 和组件拆分为两个异步 chunk,这样就可以并行加载:

tsx 复制代码
const routes = [
  {
    path: "projects",
    async loader({ request, params }) {
      let { loader } = await import("./projects-loader");
      return loader({ request, params });
    },
    lazy: () => import("./projects-component"),
  },
];

效果如下:

但是有时 data fetch 的逻辑很少,没必要单独分包,则可以用下面的方式:

tsx 复制代码
const routes = [
  {
    path: "projects",
    loader: ({ request }) => fetchDataForUrl(request.url),
    lazy: () => import("./projects-component"),
  },
];

效果如下:

以上的分包策略对嵌套路由非常有用,可最大限度减少瀑布流问题。但是如果路由嵌套层级比较深,每一级路由都需要加载异步 chunk、接口请求,可能会造成并行网络请求过多。React Router 也可以支持将所有子路由代码都合并到一个异步 chunk:

tsx 复制代码
// Assume pages/Dashboard.jsx has all of our loaders/components for multiple
// dashboard routes
let dashboardRoute = {
  path: "dashboard",
  async lazy() {
    let { Layout } = await import("./pages/Dashboard");
    return { Component: Layout };
  },
  children: [
    {
      index: true,
      async lazy() {
        let { Index } = await import("./pages/Dashboard");
        return { Component: Index };
      },
    },
    {
      path: "messages",
      async lazy() {
        let { messagesLoader, Messages } = await import(
          "./pages/Dashboard"
        );
        return {
          loader: messagesLoader,
          Component: Messages,
        };
      },
    },
  ],
};

总结一下,实际异步 chunk prefetch 在 Webpack 里面也提供了解决方案,Webpack 打包可借助 /* webpackPrefetch: true */ 魔法注释实现 prefetch,在浏览器空闲的时候下载异步 chunk。对异步 chunk 进行 prefetch 可以提升路由导航的性能,但是也存在缺点,容易造成网络请求过多,预加载的异步模块过多,可能会导致浏览器的内存占用过高,进而影响页面性能,另外加载的只能是 JS 代码,不能是接口请求,还是存在瀑布流问题。React Router 6.4+ 在路由层面整合了数据流、Lazy Loading Routes 等功能,不仅支持路由层面 data fetch,还可以支持并行加载嵌套路由的异步 chunk,把异步 chunk 和接口请求视为同等优先级加载,最大限度避免了瀑布流问题。相比 Webpack 的 prefetch,Lazy Loading Routes 不是在浏览器空闲的时候 prefetch,而是根据需要渲染的路由,按需加载异步 chunk,缺点就是写法比较灵活,开发者需要对 Code-Splitting 分包有较为深入的理解,针对各种场景分包有相关实践经验。

参考:

remix.run/blog/lazy-l... reactrouter.com/en/main/rou...

相关推荐
鑫~阳1 小时前
html + css 淘宝网实战
前端·css·html
Catherinemin1 小时前
CSS|14 z-index
前端·css
2401_882727573 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
NoneCoder3 小时前
CSS系列(36)-- Containment详解
前端·css
anyup_前端梦工厂3 小时前
初始 ShellJS:一个 Node.js 命令行工具集合
前端·javascript·node.js
5hand3 小时前
Element-ui的使用教程 基于HBuilder X
前端·javascript·vue.js·elementui
GDAL4 小时前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js
六卿4 小时前
react防止页面崩溃
前端·react.js·前端框架
z千鑫4 小时前
【前端】详解前端三大主流框架:React、Vue与Angular的比较与选择
前端·vue.js·react.js
m0_748256145 小时前
前端 MYTED单篇TED词汇学习功能优化
前端·学习