浅谈 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...

相关推荐
kingwebo'sZone3 分钟前
C#使用Aspose.Words把 word转成图片
前端·c#·word
xjt_090122 分钟前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农34 分钟前
Vue 2.3
前端·javascript·vue.js
夜郎king1 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳1 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_2 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝2 小时前
RBAC前端架构-01:项目初始化
前端·架构
程序员agions2 小时前
2026年,微前端终于“死“了
前端·状态模式