React Router 6.4 코드 분할

Remix 팀은 Remix의 모든 장점을 React Router로 가져오는 업데이트6.4에서 새로운 "데이터 라우터"를 출시했습니다. 이 업데이트를 통해 마이그레이션할 수 없는 단일 페이지 응용 프로그램은 Remix가 제공하는 많은 멋진 기능을 활용할 수 있습니다.

지금까지 최고의 기능은 병렬 데이터 가져오기, "Spinnageddon"네트워크 워터폴 제거 및 페이지 로드 개선입니다.



이 평행한 녹색 막대는 멋져 보입니다! 사용자도 기뻐할 것입니다.
  • 컨텐츠 레이아웃 이동 없음
  • 더 빠른 데이터 로드.
  • 단일 로드 스피너(클라이언트 측 렌더링으로 인해 리믹스 없이는 피할 수 없지만 여러 개보다 낫습니다).

  • 모든 것이 훌륭하게 들리는데 무엇이 빠졌습니까?

    새로운 데이터 라우터와 데이터가 필요한 경로는 애플리케이션의 최상위 수준에서 로더와 구성 요소를 정의해야 하며 전체 앱을 하나의 대규모 번들에 넣습니다 🤔

    페이지에 도착했을 때 수백 MB의 JavaScript는 성능 면에서 절대 안 됩니다. 그렇다면 병렬 데이터 가져오기 및 콘텐츠 레이아웃 이동 없음의 이점을 유지하면서 데이터 라우터 애플리케이션을 코드 분할하려면 어떻게 해야 할까요?

    옵션 1



    동일한 파일에 로더 및 구성 요소가 있는 경로 모듈:



    이 옵션은 모든 경로가 동일한 파일에서 ComponentLoader를 모두 내보낸 다음 Route Module 파일을 사용하여 지연 로드하고 이러한 파일을 동적으로 가져와서 코드 분할을 활성화하는 Remix 파일 규칙을 따릅니다.

    export async function loader(args) {
      let actualLoader = await import("./actualModule").loader;
      return actualLoader(args);
    }
    
    export const Component = React.lazy(() => import("./actualModule").default);
    
    



    import * as Tasks from "./tasks.route";
    
    // ...
    <Route
      path="/tasks"
      element={
         <React.Suspense fallback="loading...">
           <Tasks.Component />
         </React.Suspense>
      }
      loader={Tasks.loader}
    />;
    


    이 결과 네트워크 폭포:


    장점:
  • 이것은 정말 깨끗한 패턴이며 캡슐화하여 고유한 경로 구성을 구축할 수 있습니다.
  • Remix 파일 규칙에 더 가깝기 때문에 나중에 마이그레이션하기가 더 쉽습니다.

  • 단점:
  • 구성 요소 번들이 로드될 때까지 데이터 로드 및 fetch가 지연됩니다.
  • 서스펜스 경계 때문에 콘텐츠 레이아웃 이동이 발생합니다.

  • 옵션 2


    로더를 별도의 파일로 이동



    구성 요소 파일에서 로더를 분리하면 데이터가 로드되는 동시에 구성 요소를 코드 분할하고 로더에서 가져올 수 있으므로 순차적 다운로드가 제거되고 병렬화를 최대한 활용할 수 있습니다.


    actual-loader를 별도의 파일로 이동합니다.

    export async function loader() {
      // import the Component here but don't await it
      import("./actualModule");
      return await fetch('/api');
    }
    


    React.lazy가 실제로 자신을 마운트하고 호출할 때import('./actualModule') 기존 다운로드에 래치되어야 합니다.

    Disclaimer: Most modern bundlers should support the theory above, but it is not guaranteed.



    장점:
  • 구성 요소와 로더는 별도의 파일로 코드를 분할할 수 있습니다.
  • 구성 요소의 javascript 번들과 데이터 가져오기가 동시에 시작됩니다. 이는 네트워크에서 로드하는 데 시간이 오래 걸릴 수 있는 무거운 구성 요소에 특히 유용합니다.

  • 단점:
  • 이 접근 방식은 Remix 파일 규칙에서 벗어나므로 Remix로 마이그레이션하는 경우 로더를 이동하기 위한 작은 리팩터링이 필요할 수 있습니다
  • .
  • 로더가 완료될 때 구성 요소 지연 가져오기 약속이 잠재적으로 해결되었을 수 있지만 서스펜스 경계가 여전히 해결된 값을 언래핑해야 하는 경우에도 콘텐츠 레이아웃 이동이 계속 발생합니다.

  • 콘텐츠 레이아웃 이동 제거



    두 옵션 모두 장점이 있지만 둘 다 콘텐츠 레이아웃 변경 문제가 있습니다. CLS를 제거하는 것은 React Router 및 Remix의 주요 성능 이점 중 하나이지만 이제 코드 분할을 도입했기 때문에 CLS를 피할 수 없게 만듭니다... 아니면 그렇습니까?

    Remix 팀에서 코드 분할 시 CLS를 제거하는 방법에 대한 정말 멋진 요령을 제시했으며 가장 좋은 점은 위에서 언급한 두 솔루션 모두에 대해 작동한다는 것입니다.

    // Assume you want to do this in your routes, which are in the critical path JS bundle
    <Route path="lazy" loader={lazyLoader} element={<Lazy />} />
    
    // And assume you have two files containing your actual load and component:
    // lazy-loader.ts -> exports the loader
    // lazy-component.ts -> exports the component
    
    // We'll render the component via React.lazy()
    let LazyActual = React.lazy(() => import("./lazy-component"));
    
    function Lazy() {
      return (
        <React.Suspense fallback={<p>Loading component...</p>}>
          <LazyActual />
        </React.Suspense>
      );
    }
    
    // The loader is where things get interesting, we need to load the JS chunk 
    // containing our actual loader code - BUT we also want to get a head start 
    // on downloading the component chunk instead of waiting for React.lazy() to 
    // kick it off.  Waterfalls are bad!  This opens up the possibility of the 
    // component chunk finishing _before_ the loader chunk + execution.  If that 
    // happens we don't want to see a small CLS flicker from Suspense since the 
    // component _is already downloaded_!
    export async function lazyLoader(...args) {
      let controller = new AbortController();
    
      /*
       * Kick off our component chunk load but don't await it
       * This allows us to parallelize the component download with loader 
       * download and execution.
       *
       * Normal React.lazy()
       * 
       *   load loader.ts     execute loader()   load component.ts
       *   -----------------> -----------------> ----------------->
       *
       * Kicking off the component load _in_ your loader()
       * 
       *   load loader.ts     execute loader()   
       *   -----------------> -----------------> 
       *                      load component.ts
       *                      ----------------->
       *
       * Kicking off the component load _alongside_ your loader.ts chunk load
       * 
       *   load loader.ts     execute loader()   
       *   -----------------> -----------------> 
       *   load component.ts
       *   ----------------->
       */
      import("./lazy-component").then(
        (componentModule) => {
          if (!controller.signal.aborted) {
            // We loaded the component _before_ our loader finished, so we can
            // blow away React.lazy and just use the component directly.  This 
            // avoids the flicker we'd otherwise get since React.lazy would need 
            // to throw the already-resolved promise up to the Suspense boundary 
            // one time to get the resolved value
            LazyActual = componentModule.default;
          }
        },
        () => {}
      );
    
      try {
        // Load our loader chunk
        let { default: loader } = await import("./lazy-loader");
        // Call the loader
        return await loader(...args);
      } finally {
        // Abort the controller when our loader finishes.  If we finish before the 
        // component chunk loads, this will ensure we still use React.lazy to 
        // render the component since it's not yet available.  If the component 
        // chunk finishes first, it will have overwritten Lazy with the legit 
        // component so we'll never see the suspense fallback
        controller.abort();
      }
    }
    


    Source Code

    이 문제를 해결하는 데 도움을 준 Remix에 다시 한 번 감사드립니다! 모든 신용은 그에게 돌아갑니다.

    좋은 웹페이지 즐겨찾기