코드 분할 자바스크립트

큰 자바스크립트 리소스를 로드하면 페이지 속도에 상당한 영향을 미칩니다. 자바스크립트를 작은 단위로 분할하고 시작 시 페이지가 작동하는 데 필요한 항목만 다운로드하면 페이지의 로드 반응성이 크게 개선되어 페이지의 다음 페인트와의 상호작용 (INP)이 개선될 수 있습니다.

페이지에서 대용량 자바스크립트 파일을 다운로드, 파싱, 컴파일하는 동안 일정 시간 동안 응답하지 않을 수 있습니다. 페이지 요소는 페이지의 초기 HTML의 일부이며 CSS에 의해 스타일이 지정되므로 표시됩니다. 그러나 이러한 상호작용 요소 및 페이지에서 로드하는 다른 스크립트를 구동하는 데 필요한 자바스크립트가 자바스크립트를 파싱하고 실행할 수 있기 때문입니다. 따라서 사용자는 상호작용이 상당히 지연되었거나 심지어 완전히 끊어진 것처럼 느껴질 수 있습니다.

이 문제는 자바스크립트가 기본 스레드에서 파싱되고 컴파일될 때 기본 스레드가 차단되어 발생하는 경우가 많습니다. 이 프로세스가 너무 오래 걸리면 대화형 페이지 요소가 사용자 입력에 충분히 빠르게 반응하지 않을 수 있습니다. 이 문제를 해결하는 한 가지 방법은 페이지가 작동하는 데 필요한 자바스크립트만 로드하고, 코드 분할이라는 기법을 통해 나중에 로드하도록 다른 자바스크립트를 지연시키는 것입니다. 이 모듈에서는 이 두 가지 기법 중 후자를 중점적으로 살펴봅니다.

코드 분할을 통해 시작 시 JavaScript 파싱 및 실행 줄이기

Lighthouse자바스크립트 실행이 2초 이상 걸리면 경고를 표시하고 3.5초 넘게 걸리면 실패합니다. 과도한 자바스크립트 파싱 및 실행은 페이지 수명 주기의 어느 시점에서든 잠재적인 문제가 될 수 있습니다. 사용자가 페이지와 상호작용하는 시간이 자바스크립트를 처리하고 실행하는 기본 스레드 작업이 실행되는 순간과 일치하면 상호작용의 입력 지연을 증가시킬 수 있기 때문입니다.

게다가 과도한 자바스크립트 실행과 파싱은 페이지 수명 주기에서 사용자가 페이지와 상호작용할 가능성이 매우 높기 때문에 초기 페이지 로드 중에 특히 문제가 됩니다. 실제로 로드 응답성 측정항목인 총 차단 시간 (TBT)INP높은 상관관계가 있으므로 사용자가 초기 페이지 로드 중에 상호작용을 시도하는 경향이 높음을 나타냅니다.

페이지에서 요청하는 각 자바스크립트 파일을 실행하는 데 소요된 시간을 보고하는 Lighthouse 감사는 코드 분할의 후보가 될 수 있는 스크립트를 정확하게 식별하는 데 유용합니다. 그런 다음 Chrome DevTools의 커버리지 도구를 사용해 페이지 로드 중에 페이지 자바스크립트의 어떤 부분이 사용되지 않는지 정확히 파악할 수 있습니다.

코드 분할은 페이지의 초기 JavaScript 페이로드를 줄일 수 있는 유용한 기법입니다. 이 라이브러리를 사용하면 JavaScript 번들을 다음 두 부분으로 분할할 수 있습니다.

  • 페이지 로드에 필요한 자바스크립트이므로 다른 시점에는 로드할 수 없습니다.
  • 이후 시점에, 사용자가 페이지의 특정 상호작용 요소와 상호작용하는 시점에 로드할 수 있는 나머지 자바스크립트

코드 분할은 동적 import() 구문을 사용하여 수행할 수 있습니다. 이 구문은 시작 시 지정된 자바스크립트 리소스를 요청하는 <script> 요소와 달리 페이지 수명 주기 중 이후 시점에 자바스크립트 리소스를 요청합니다.

document.querySelectorAll('#myForm input').addEventListener('blur', async () => {
  // Get the form validation named export from the module through destructuring:
  const { validateForm } = await import('/validate-form.mjs');

  // Validate the form:
  validateForm();
}, { once: true });

위의 자바스크립트 스니펫에서 validate-form.mjs 모듈은 사용자가 양식의 <input> 필드를 흐리게 처리할 때만 다운로드, 파싱, 실행됩니다. 이 상황에서 양식의 유효성 검사 로직을 구동하는 JavaScript 리소스는 실제로 사용될 가능성이 가장 높은 페이지에만 관련됩니다.

webpack, Parcel, Rollup, esbuild와 같은 JavaScript 번들러는 소스 코드에서 동적 import() 호출이 발생할 때마다 JavaScript 번들을 더 작은 단위로 분할하도록 구성할 수 있습니다. 이러한 도구는 대부분 이 작업을 자동으로 수행하지만, 특히 esbuild의 경우 이 최적화를 선택해야 합니다.

코드 분할에 관한 유용한 참고 사항

코드 분할은 초기 페이지 로드 중에 기본 스레드 경합을 줄이는 효과적인 방법이지만 JavaScript 소스 코드를 감사하여 코드 분할 기회를 발견하려는 경우 몇 가지 사항에 유의해야 합니다.

가능한 경우 번들러 사용

개발자는 개발 프로세스 중에 JavaScript 모듈을 사용하는 것이 일반적입니다. 이는 코드 가독성과 유지관리성을 개선하는 훌륭한 개발자 환경 개선입니다. 그러나 JavaScript 모듈을 프로덕션에 제공할 때 차선의 성능 특성이 발생할 수 있습니다.

가장 중요한 점은 코드를 분할하려는 모듈을 포함하여 소스 코드를 처리하고 최적화하는 데 번들러를 사용해야 한다는 것입니다. 번들러는 JavaScript 소스 코드에 최적화를 적용하는 데 매우 효과적일 뿐만 아니라 번들 크기와 압축 비율 대비 성능 고려사항의 균형을 맞추는 데도 매우 효과적입니다. 압축 효과는 번들 크기에 따라 증가하지만 번들러는 스크립트 평가로 인해 긴 작업이 발생할 정도로 번들이 너무 크지 않은지 확인합니다.

또한 번들러는 네트워크를 통해 번들로 묶이지 않은 다수의 모듈을 배송하는 문제를 피할 수 있습니다. 자바스크립트 모듈을 사용하는 아키텍처는 크고 복잡한 모듈 트리를 사용하는 경향이 있습니다. 모듈 트리가 번들로 묶이지 않으면 각 모듈이 별도의 HTTP 요청을 나타내며 모듈을 번들로 묶지 않으면 웹 앱에서의 상호작용이 지연될 수 있습니다. <link rel="modulepreload"> 리소스 힌트를 사용하여 큰 모듈 트리를 최대한 빨리 로드할 수도 있지만 로드 성능 관점에서는 JavaScript 번들을 사용하는 것이 더 좋습니다.

스트리밍 컴파일을 의도치 않게 사용 중지하지 않음

Chromium의 V8 자바스크립트 엔진은 프로덕션 자바스크립트 코드가 최대한 효율적으로 로드될 수 있도록 즉시 사용 가능한 다양한 최적화 기능을 제공합니다. 이러한 최적화 중 하나가 스트리밍 컴파일이라고 하며, 브라우저로 스트리밍된 HTML의 증분 파싱과 마찬가지로 네트워크에서 도착하는 JavaScript의 스트리밍 청크를 컴파일합니다.

Chromium에서 웹 애플리케이션에 대해 스트리밍 컴파일이 실행되도록 하는 방법에는 두 가지가 있습니다.

  • 자바스크립트 모듈을 사용하지 않도록 프로덕션 코드를 변환합니다. 번들러는 컴파일 대상을 기준으로 자바스크립트 소스 코드를 변환할 수 있으며, 대상은 지정된 환경에 따라 달라지는 경우가 많습니다. V8은 모듈을 사용하지 않는 모든 JavaScript 코드에 스트리밍 컴파일을 적용하며 개발자는 JavaScript 모듈 코드를 JavaScript 모듈과 그 기능을 사용하지 않는 구문으로 변환하도록 번들러를 구성할 수 있습니다.
  • 자바스크립트 모듈을 프로덕션에 제공하려면 .mjs 확장 프로그램을 사용하세요. 프로덕션 JavaScript에서 모듈을 사용하는지 여부와 관계없이, 모듈을 사용하는 JavaScript와 사용하지 않는 JavaScript에는 특별한 콘텐츠 유형이 없습니다. V8에 관해서는 .js 확장 프로그램을 사용하여 프로덕션 환경에 JavaScript 모듈을 출시할 때 스트리밍 컴파일을 효과적으로 선택 해제할 수 있습니다. JavaScript 모듈에 .mjs 확장 프로그램을 사용하면 V8은 모듈 기반 JavaScript 코드의 스트리밍 컴파일이 중단되지 않도록 할 수 있습니다.

이러한 고려사항 때문에 코드 분할 사용을 망설이지 마세요. 코드 분할은 사용자에 대한 초기 자바스크립트 페이로드를 줄이는 효과적인 방법이지만, 번들러를 사용하고 V8의 스트리밍 컴파일 동작을 보존하는 방법을 알면 프로덕션 JavaScript 코드의 속도를 사용자에게 최대한 높일 수 있습니다.

동적 가져오기 데모

webpack

webpack은 번들러가 JavaScript 파일을 분할하는 방법을 구성할 수 있는 SplitChunksPlugin라는 플러그인과 함께 제공됩니다. webpack은 동적 import() 및 정적 import 문을 모두 인식합니다. SplitChunksPlugin의 동작은 구성에서 chunks 옵션을 지정하여 수정할 수 있습니다.

  • chunks: async는 기본값이며 동적 import() 호출을 참조합니다.
  • chunks: initial는 정적 import 호출을 나타냅니다.
  • chunks: all는 동적 import() 및 정적 가져오기를 모두 지원하므로 async 가져오기와 initial 가져오기 간에 단위를 공유할 수 있습니다.

기본적으로 webpack에서 동적 import() 문을 발견할 때마다 해당 모듈을 위한 별도의 청크를 만듭니다.

/* main.js */

// An application-specific chunk required during the initial page load:
import myFunction from './my-function.js';

myFunction('Hello world!');

// If a specific condition is met, a separate chunk is downloaded on demand,
// rather than being bundled with the initial chunk:
if (condition) {
  // Assumes top-level await is available. More info:
  // https://v8.dev/features/top-level-await
  await import('/form-validation.js');
}

위 코드 스니펫의 기본 webpack 구성은 다음과 같은 두 개의 개별 청크로 이루어집니다.

  • webpack이 initial 청크로 분류되는 main.js 청크에는 main.js./my-function.js 모듈이 포함됩니다.
  • form-validation.js만 포함된 async 청크 (구성된 경우 리소스 이름에 파일 해시 포함) 이 청크는 conditiontruthy일 때만 다운로드됩니다.

이 구성을 사용하면 실제로 필요할 때까지 form-validation.js 청크 로드를 연기할 수 있습니다. 이렇게 하면 초기 페이지 로드 중 스크립트 평가 시간을 줄여 로드 응답성을 개선할 수 있습니다. form-validation.js 청크의 스크립트 다운로드 및 평가는 지정된 조건이 충족될 때 발생하며, 이 경우 동적으로 가져온 모듈이 다운로드됩니다. 한 가지 예는 특정 브라우저에만 polyfill이 다운로드되거나 이전 예에서와 같이 사용자 상호작용에 필요한 모듈이 필요한 경우일 수 있습니다.

반면 SplitChunksPlugin 구성을 변경하여 chunks: initial을 지정하면 코드가 초기 청크에서만 분할됩니다. 이러한 청크는 정적으로 가져오거나 webpack의 entry 속성에 나열되는 것입니다. 앞의 예에서 보면 결과 청크는 단일 스크립트 파일의 form-validation.js main.js의 조합으로, 결과적으로 초기 페이지 로드 성능이 저하될 수 있습니다.

SplitChunksPlugin 옵션은 큰 스크립트를 여러 개의 작은 스크립트로 구분하도록 구성할 수도 있습니다. 예를 들어, maxSize 옵션을 사용하여 청크가 maxSize에 지정된 값을 초과하면 청크를 별도의 파일로 분할하도록 Webpack에 지시할 수 있습니다. 큰 스크립트 파일을 작은 파일로 나누면 로드 응답성이 개선될 수 있습니다. CPU 집약적인 스크립트 평가 작업은 더 작은 작업으로 나뉘어 오랫동안 기본 스레드를 차단할 가능성이 낮아지기 때문입니다.

또한 더 큰 JavaScript 파일을 생성하면 스크립트에 캐시 무효화가 발생할 가능성이 높아집니다. 예를 들어 프레임워크와 퍼스트 파티 애플리케이션 코드가 모두 포함된 매우 큰 스크립트를 제공하는 경우 프레임워크만 업데이트되고 번들 리소스에는 다른 내용이 없으면 전체 번들이 무효화될 수 있습니다.

반면 스크립트 파일이 작을수록 재방문자가 캐시에서 리소스를 검색할 가능성이 커지므로 재방문 시 페이지 로드가 빨라집니다. 그러나 파일이 작을수록 큰 파일보다 압축의 이점이 적고 준비되지 않은 브라우저 캐시로 페이지 로드 시 네트워크 왕복 시간이 늘어날 수 있습니다. 캐싱 효율성, 압축 효과, 스크립트 평가 시간 간의 균형을 유지하도록 주의를 기울여야 합니다.

webpack 데모

webpack SplitChunksPlugin 데모.

학습한 내용 테스트

코드 분할을 수행할 때 어떤 유형의 import 문이 사용되나요?

동적 import()
정답입니다.
정적 import.
다시 시도해 주세요.

다음 중 자바스크립트 모듈의 상단에 위치해야 하는 import 문 유형을 반드시 다른 위치에 배치해서는 안 되나요?

동적 import()
다시 시도해 주세요.
정적 import.
정답입니다.

webpack에서 SplitChunksPlugin를 사용할 때 async 청크와 initial 청크의 차이점은 무엇인가요?

async 청크는 동적 import()를 사용하여 로드되고 initial 청크는 정적 import를 사용하여 로드됩니다.
정답입니다.
async 청크는 정적 import를 사용하여 로드되고 initial 청크는 동적 import()를 사용하여 로드됩니다.
다시 시도해 주세요.

다음: 이미지 및 <iframe> 요소 지연 로드

자바스크립트는 비용이 많이 드는 리소스 유형인 경향이 있지만, 로드를 지연시킬 수 있는 리소스 유형은 JavaScript가 유일합니다. 이미지와 <iframe> 요소는 그 자체로 잠재적으로 비용이 많이 드는 리소스입니다. 자바스크립트와 마찬가지로 이미지 및 <iframe> 요소의 로드를 지연 로드하여 지연할 수 있습니다. 자세한 내용은 이 과정의 다음 모듈에서 설명합니다.