세분화된 청크 분할로 Next.js 및 Gatsby 페이지 로드 성능 개선

Next.js와 Gatsby의 최신 웹팩 청크 전략은 중복 코드를 최소화하여 페이지 로드 성능을 개선합니다.

Chrome은 JavaScript 오픈소스 생태계의 도구 및 프레임워크를 사용하여 공동작업을 수행합니다. Next.jsGatsby의 로드 성능을 개선하기 위해 최근 여러 가지 새로운 최적화가 추가되었습니다. 이 문서에서는 현재 두 프레임워크에서 기본적으로 제공되는 개선된 세분화된 청크 전략을 다룹니다.

소개

많은 웹 프레임워크와 마찬가지로 Next.js 및 Gatsby는 webpack을 핵심 번들러로 사용합니다. webpack v3에서는 CommonsChunkPlugin를 도입하여 여러 진입점 간에 공유된 모듈을 단일 (또는 몇 개의) '커먼' 청크 (또는 청크)로 출력할 수 있도록 했습니다. 공유 코드는 별도로 다운로드하여 브라우저 캐시에 미리 저장할 수 있으므로 로드 성능이 향상될 수 있습니다.

이 패턴은 다음과 같은 진입점 및 번들 구성을 사용하는 많은 단일 페이지 애플리케이션 프레임워크에서 널리 사용되고 있습니다.

공통 진입점 및 번들 구성

실용적이지만 모든 공유 모듈 코드를 단일 청크로 번들로 묶는 개념에는 한계가 있습니다. 모든 진입점에서 공유되지 않는 모듈은 모듈을 사용하지 않는 경로에 대해 다운로드할 수 있으며, 이로 인해 필요한 것보다 더 많은 코드가 다운로드될 수 있습니다. 예를 들어 page1common 청크를 로드할 때 page1moduleC를 사용하지 않더라도 moduleC 코드를 로드합니다. 이러한 이유로 Webpack v4는 일부 다른 플러그인과 함께 이 플러그인을 삭제하고 새로운 플러그인인 SplitChunksPlugin로 대체했습니다.

청크 개선

대부분의 사용자에게는 SplitChunksPlugin의 기본 설정이 적합합니다. 여러 conditions에 따라 여러 경로에서 중복된 코드를 가져오는 것을 방지하기 위해 여러 분할 청크가 생성됩니다.

그러나 이 플러그인을 사용하는 많은 웹 프레임워크는 여전히 청크 분할에 '단일 공통' 접근 방식을 따릅니다. 예를 들어 Next.js는 페이지의 50% 이상에서 사용되는 모듈과 모든 프레임워크 종속 항목 (react, react-dom 등)이 포함된 commons 번들을 생성합니다.

const splitChunksConfigs = {
  …
  prod: {
    chunks: 'all',
    cacheGroups: {
      default: false,
      vendors: false,
      commons: {
        name: 'commons',
        chunks: 'all',
        minChunks: totalPages > 2 ? totalPages * 0.5 : 2,
      },
      react: {
        name: 'commons',
        chunks: 'all',
        test: /[\\/]node_modules[\\/](react|react-dom|scheduler|use-subscription)[\\/]/,
      },
    },
  },

프레임워크 종속 코드를 공유 청크에 포함하면 모든 진입점에 대해 이 코드를 다운로드하고 캐시할 수 있지만, 절반의 페이지 이상에서 사용되는 일반 모듈을 포함하는 사용 기반 휴리스틱은 그다지 효과적이지 않습니다. 이 비율을 수정하면 다음 두 가지 결과 중 하나만 생성됩니다.

  • 비율을 줄이면 더 많은 불필요한 코드가 다운로드됩니다.
  • 비율을 높이면 여러 경로에서 더 많은 코드가 중복됩니다.

이 문제를 해결하기 위해 Next.js는 모든 경로의 불필요한 코드를 줄이는 SplitChunksPlugin다른 구성을 채택했습니다.

  • 충분히 큰 서드 파티 모듈 (160KB 초과)은 자체 개별 단위로 분할됩니다.
  • 프레임워크 종속 항목 (react, react-dom 등)을 위해 별도의 frameworks 청크가 생성됩니다.
  • 필요한 만큼의 공유 청크를 생성할 수 있음 (최대 25개)
  • 청크가 생성될 최소 크기가 20KB로 변경됨

이 세분화된 청크 전략은 다음과 같은 이점을 제공합니다.

  • 페이지 로드 시간 개선. 단일 청크 대신 여러 공유 청크를 내보내면 진입점에서 불필요한 (또는 중복) 코드의 양을 최소화할 수 있습니다.
  • 탐색 중 캐싱 개선. 대규모 라이브러리와 프레임워크 종속 항목을 별도의 청크로 분할하면 둘 다 업그레이드가 수행될 때까지 변경될 가능성이 낮기 때문에 캐시 무효화 가능성이 줄어듭니다.

Next.js가 webpack-config.ts에서 채택한 전체 구성을 확인할 수 있습니다.

추가 HTTP 요청

SplitChunksPlugin는 세분화된 청크 분할의 기반을 정의했으며 이 접근 방식을 Next.js와 같은 프레임워크에 적용하는 것은 완전히 새로운 개념은 아니었습니다. 그러나 많은 프레임워크는 몇 가지 이유로 여전히 단일 휴리스틱 및 '일반' 번들 전략을 계속 사용했습니다. 여기에는 더 많은 HTTP 요청이 사이트 성능에 부정적인 영향을 미칠 수 있다는 우려도 포함됩니다.

브라우저는 단일 출처 (Chrome의 경우 6)에 대한 제한된 수의 TCP 연결만 열 수 있으므로 번들러에서 출력하는 청크 수를 최소화하면 총 요청 수를 이 임계값 미만으로 유지할 수 있습니다. 그러나 이 내용은 HTTP/1.1에만 적용됩니다. HTTP/2의 멀티플렉싱을 사용하면 단일 출처에서 단일 연결을 사용하여 여러 요청을 병렬로 스트리밍할 수 있습니다. 즉, 일반적으로 번들러에서 내보내는 청크 수를 제한하는 것에 대해 걱정할 필요가 없습니다.

모든 주요 브라우저는 HTTP/2를 지원합니다. Chrome 및 Next.js팀은 Next.js의 단일 'Commons' 번들을 여러 개의 공유 청크로 분할하여 요청 수를 늘리면 로드 성능에 어떤 식으로든 영향을 미칠지 확인하고자 했습니다. 먼저 maxInitialRequests 속성을 사용하여 최대 동시 요청 수를 수정하면서 단일 사이트의 성능을 측정했습니다.

요청 수 증가에 따른 페이지 로드 성능

단일 웹페이지에서 여러 시도를 평균 3회 실행하여 최대 초기 요청 수 (5~15)를 변경했을 때 load, 렌더링 시작, 첫 콘텐츠 렌더링 시간은 모두 거의 동일하게 유지되었습니다. 흥미롭게도 공격적으로 수백 개의 요청으로 분할한 후에만 약간의 성능 오버헤드가 발견되었습니다.

수백 건의 요청을 통한 페이지 로드 성능

이를 통해 신뢰할 수 있는 기준 (20~25개의 요청)을 유지하면 로드 성능과 캐싱 효율성 간에 올바른 균형을 유지할 수 있었습니다. 기준 테스트 후 25개가 maxInitialRequest 수로 선택되었습니다.

동시에 발생하는 최대 요청 수를 수정하면 2개 이상의 공유 번들이 만들어졌고 각 진입점에 맞게 이를 적절히 분리하면 동일한 페이지에 필요하지 않은 코드 양이 크게 줄었습니다.

청크화 증가를 통한 JavaScript 페이로드 감소

이 실험은 요청 수를 수정하여 페이지 로드 성능에 부정적인 영향이 있는지 확인하기 위한 것이었습니다. 테스트 결과, 테스트 페이지에서 maxInitialRequests25로 설정하는 것이 페이지 속도를 저하시키지 않고 JavaScript 페이로드 크기를 줄였으므로 최적화된 것으로 나타났습니다. 페이지를 하이드레이션하는 데 필요한 자바스크립트의 총량은 여전히 거의 동일했으며, 이는 코드 양을 줄여도 페이지 로드 성능이 개선되지 않은 이유입니다.

webpack에서는 청크를 생성하기 위한 기본 최소 크기로 30KB를 사용합니다. 그러나 maxInitialRequests 값 25를 최소 크기 20KB와 결합하면 캐싱이 개선되었습니다.

세분화된 청크로 크기 축소

Next.js를 비롯한 많은 프레임워크에서 모든 경로 전환에 새로운 스크립트 태그를 삽입하기 위해 자바스크립트로 처리되는 클라이언트 측 라우팅을 사용합니다. 그렇다면 이러한 동적 청크는 빌드 시간에 어떻게 미리 결정할까요?

Next.js는 서버 측 빌드 매니페스트 파일을 사용하여 출력된 청크가 서로 다른 진입점에서 사용되는지 확인합니다. 이 정보를 클라이언트에도 제공하기 위해 모든 진입점의 모든 종속 항목을 매핑하는 요약된 클라이언트 측 빌드 매니페스트 파일을 만들었습니다.

// Returns a promise for the dependencies for a particular route
getDependencies (route) {
  return this.promisedBuildManifest.then(
    man => (man[route] && man[route].map(url => `/_next/${url}`)) || []
  )
}
Next.js 애플리케이션의 여러 공유 청크의 출력

이 새로운 세분화된 청크 전략은 처음 Next.js에서 플래그 뒤에 출시되었으며, 이 전략은 여러 얼리 어답터를 대상으로 테스트되었습니다. 많은 사용자가 전체 사이트에 사용되는 전체 JavaScript가 크게 감소한 것을 확인했습니다.

웹사이트 총 자바스크립트 변경 차이(%)
https://www.barnebys.com/ -238 KB 23% 감소
https://sumup.com/ -220 KB -30%
https://www.hashicorp.com/ -11 MB -71%
자바스크립트 크기 축소 - 모든 경로에서 (압축)

최종 버전은 기본적으로 버전 9.2에서 출시되었습니다.

개츠비

Gatsby는 사용량 기반 휴리스틱을 사용해 공통 모듈을 정의하는 것과 동일한 접근 방식을 사용했습니다.

config.optimization = {
  …
  splitChunks: {
    name: false,
    chunks: `all`,
    cacheGroups: {
      default: false,
      vendors: false,
      commons: {
        name: `commons`,
        chunks: `all`,
        // if a chunk is used more than half the components count,
        // we can assume it's pretty global
        minChunks: componentsCount > 2 ? componentsCount * 0.5 : 2,
      },
      react: {
        name: `commons`,
        chunks: `all`,
        test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
      },

또한 유사한 세분화된 청크 전략을 채택하도록 웹팩 구성을 최적화함으로써 다수의 대규모 사이트에서 자바스크립트가 상당히 감소한다는 사실도 확인했습니다.

웹사이트 총 자바스크립트 변경 차이(%)
https://www.gatsbyjs.org/ -680 KB -22%
https://www.thirdandgrove.com/ -390 KB -25%
https://ghost.org/ -1.1 MB -35%
https://reactjs.org/ -80KB -8%
자바스크립트 크기 축소 - 모든 경로에서 (압축)

PR을 살펴보고 v2.20.7에서 기본적으로 제공되는 webpack 구성에 이 로직을 구현한 방법을 알아보세요.

결론

세분화된 청크를 배송한다는 개념은 Next.js, Gatsby 또는 webpack에만 국한되지 않습니다. 사용된 프레임워크나 모듈 번들러와 상관없이 대규모 '공통' 번들 접근 방식을 따르는 경우 누구나 애플리케이션의 청크 전략을 개선하는 것을 고려해야 합니다.

  • 바닐라 React 애플리케이션에 적용된 동일한 청크 분할 최적화를 보려면 이 샘플 React 앱을 살펴보세요. 세분화된 청크 전략의 단순화된 버전을 사용하며 사이트에 동일한 종류의 로직을 적용하는 데 도움이 될 수 있습니다.
  • Rollup의 경우 기본적으로 청크가 세부적으로 생성됩니다. 동작을 수동으로 구성하려면 manualChunks를 살펴보세요.