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

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

Chrome은 JavaScript 오픈소스 생태계의 도구 및 프레임워크와 공동작업하고 있습니다. Next.jsGatsby의 로드 성능을 개선하기 위해 최근에 여러 최신 최적화가 추가되었습니다. 이 문서에서는 개선된 세분화된 청킹 전략을 다룹니다. 이제 이 전략은 두 프레임워크 모두에서 기본적으로 제공됩니다.

소개

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

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

일반적인 진입점 및 번들 구성

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

청킹 개선

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

그러나 이 플러그인을 사용하는 많은 웹 프레임워크는 여전히 청크 분할에 '단일 공용' 접근 방식을 따르고 있습니다. 예를 들어 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 요청이 사이트 성능에 부정적인 영향을 미칠 수 있다는 우려가 포함됩니다.

브라우저는 단일 출처에 대해 제한된 수의 TCP 연결(Chrome의 경우 6개)만 열 수 있으므로 번들러에서 출력하는 청크 수를 최소화하면 총 요청 수가 이 임곗값 아래에 유지될 수 있습니다. 그러나 이는 HTTP/1.1에만 적용됩니다. HTTP/2의 다중화를 사용하면 단일 출처를 통해 단일 연결을 사용하여 여러 요청을 동시에 스트리밍할 수 있습니다. 즉, 일반적으로 번들러에서 내보내는 청크 수를 제한할 필요가 없습니다.

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

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

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

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

안정적인 기준점(요청 20~25개) 미만으로 유지하면 로드 성능과 캐싱 효율성 간에 적절한 균형을 유지할 수 있음을 알 수 있습니다. 몇 가지 기준 테스트 후 25가 maxInitialRequest 개수로 선택되었습니다.

동시에 실행되는 최대 요청 수를 수정하면 공유 번들이 두 개 이상 생성되었으며, 각 진입점에 대해 적절하게 분리하여 동일한 페이지의 불필요한 코드의 양을 크게 줄였습니다.

증가된 청크로 JavaScript 페이로드 감소

이 실험은 페이지 로드 성능에 부정적인 영향이 있는지 확인하기 위해 요청 수를 수정하는 것에 대해서만 진행되었습니다. 테스트 페이지에서 maxInitialRequests25로 설정하면 페이지 속도가 느려지지 않고 JavaScript 페이로드 크기가 줄어들어 최적의 설정이라는 것을 알 수 있습니다. 페이지를 하이드라이트하는 데 필요한 총 JavaScript 양은 여전히 거의 동일하게 유지되었습니다. 따라서 코드 양이 줄어들더라도 페이지 로드 성능이 반드시 개선되지 않는 이유를 알 수 있습니다.

webpack은 생성할 청크의 기본 최소 크기로 30KB를 사용합니다. 하지만 maxInitialRequests 값 25를 20KB 최소 크기와 결합하면 캐싱이 개선되었습니다.

세분화된 청크로 크기 줄이기

Next.js를 비롯한 많은 프레임워크는 클라이언트 측 라우팅(JavaScript에서 처리)을 사용하여 모든 경로 전환에 최신 스크립트 태그를 삽입합니다. 하지만 빌드 시 이러한 동적 청크를 미리 결정하는 방법은 무엇일까요?

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가 크게 감소했습니다.

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

최종 버전은 버전 9.2에 기본적으로 포함되어 있습니다.

Gatsby

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)[\\/]/,
      },

유사한 세분화된 청크 전략을 채택하도록 webpack 구성을 최적화하여 많은 대규모 사이트에서 상당한 JavaScript 감소도 확인했습니다.

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

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

결론

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

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