Next.js 및 Gatsby의 최신 webpack 청크 전략은 중복 코드를 최소화하여 페이지 로드 성능을 개선합니다.
Chrome은 JavaScript 오픈소스 생태계의 도구 및 프레임워크와 협업하고 있습니다. 최근 Next.js 및 Gatsby의 로드 성능을 개선하기 위해 여러 최신 최적화가 추가되었습니다. 이 도움말에서는 이제 두 프레임워크에서 기본적으로 제공되는 개선된 세부 청크 전략을 다룹니다.
소개
많은 웹 프레임워크와 마찬가지로 Next.js와 Gatsby는 webpack을 핵심 번들러로 사용합니다. webpack v3에서는 단일 또는 소수의 'commons' 청크(또는 청크)에서 다양한 진입점 간에 공유되는 모듈을 출력할 수 있도록 CommonsChunkPlugin
가 도입되었습니다. 공유 코드는 별도로 다운로드하여 브라우저 캐시에 미리 저장할 수 있으므로 로드 성능이 향상될 수 있습니다.
이 패턴은 많은 단일 페이지 애플리케이션 프레임워크가 다음과 같은 진입점 및 번들 구성을 채택하면서 인기를 얻었습니다.
실용적이지만 모든 공유 모듈 코드를 단일 청크로 번들링하는 개념에는 한계가 있습니다. 모든 진입점에서 공유되지 않는 모듈은 이를 사용하지 않는 경로에 다운로드될 수 있으므로 필요한 것보다 더 많은 코드가 다운로드됩니다. 예를 들어 page1
가 common
청크를 로드할 때 page1
가 moduleC
를 사용하지 않더라도 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와 같은 프레임워크에 적용하는 것은 완전히 새로운 개념이 아니었습니다. 하지만 많은 프레임워크는 몇 가지 이유로 여전히 단일 휴리스틱과 'commons' 번들 전략을 사용했습니다. 여기에는 HTTP 요청이 많아지면 사이트 성능에 부정적인 영향을 미칠 수 있다는 우려가 포함됩니다.
브라우저는 단일 출처에 대해 제한된 수의 TCP 연결 (Chrome의 경우 6개)만 열 수 있으므로 번들러에서 출력하는 청크 수를 최소화하면 총 요청 수가 이 기준점을 넘지 않도록 할 수 있습니다. 하지만 이는 HTTP/1.1에만 해당합니다. HTTP/2의 다중화를 사용하면 단일 출처를 통해 단일 연결을 사용하여 여러 요청을 동시에 스트리밍할 수 있습니다. 즉, 일반적으로 번들러에서 내보낸 청크 수를 제한하는 것에 대해 걱정할 필요가 없습니다.
모든 주요 브라우저가 HTTP/2를 지원합니다. Chrome 및 Next.js팀은 Next.js의 단일 'commons' 번들을 여러 공유 청크로 분할하여 요청 수를 늘리면 로드 성능에 어떤 영향을 미치는지 확인하고 싶었습니다. maxInitialRequests
속성을 사용하여 최대 동시 요청 수를 수정하면서 단일 사이트의 성능을 측정하는 것으로 시작했습니다.
단일 웹페이지에서 여러 시험을 3회 실행한 평균에서 최대 초기 요청 수 (5~15)를 변경할 때 load
, start-render, 콘텐츠가 포함된 첫 페인트 시간이 모두 거의 동일하게 유지되었습니다. 흥미롭게도 수백 개의 요청으로 적극적으로 분할한 후에만 약간의 성능 오버헤드가 발생했습니다.
이를 통해 안정적인 기준점 (20~25개 요청)을 유지하는 것이 로드 성능과 캐싱 효율성 간의 적절한 균형을 유지하는 것으로 나타났습니다. 기준 테스트 후 25가 maxInitialRequest
개수로 선택되었습니다.
병렬로 발생하는 최대 요청 수를 수정하면 공유 번들이 두 개 이상이 되었고 각 진입점에 맞게 적절히 분리하면 동일한 페이지의 불필요한 코드 양이 크게 줄었습니다.
이 실험은 페이지 로드 성능에 부정적인 영향이 있는지 확인하기 위해 요청 수를 수정하는 데에만 초점을 맞췄습니다. 테스트 페이지에서 maxInitialRequests
을 25
로 설정하는 것이 페이지 속도를 늦추지 않고 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에서 플래그 뒤에 처음 출시되었으며, 여기서 여러 초기 도입자를 대상으로 테스트되었습니다. 많은 사용자가 전체 사이트에 사용되는 총 JavaScript가 크게 감소했습니다.
웹사이트 | 총 JS 변경사항 | 차이(%) |
---|---|---|
https://www.barnebys.com/ | -238 KB | -23% |
https://sumup.com/ | -220 KB | -30% |
https://www.hashicorp.com/ | -11 MB | -71% |
최종 버전은 버전 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/ | -680 KB | -22% |
https://www.thirdandgrove.com/ | -390 KB | -25% |
https://ghost.org/ | -1.1 MB | -35% |
https://reactjs.org/ | -80 Kb | -8% |
PR을 살펴보고 v2.20.7에서 기본적으로 제공되는 webpack 구성에 이 로직이 어떻게 구현되었는지 알아보세요.
결론
세부적인 청크를 전송하는 개념은 Next.js, Gatsby 또는 webpack에만 해당하지 않습니다. 사용하는 프레임워크나 모듈 번들러와 관계없이 큰 'commons' 번들 접근 방식을 따르는 경우 모든 사람이 애플리케이션의 청킹 전략을 개선하는 것을 고려해야 합니다.
- 일반 React 애플리케이션에 동일한 청크 최적화를 적용하려면 이 샘플 React 앱을 참고하세요. 세부적인 청크 전략의 간소화된 버전을 사용하며 사이트에 동일한 종류의 로직을 적용하는 데 도움이 될 수 있습니다.
- Rollup의 경우 기본적으로 청크가 세부적으로 생성됩니다. 동작을 수동으로 구성하려면
manualChunks
을 참고하세요.