[Google I/O '18 Web performance made easy](https://youtu.be/Mv-l3-tJgGk) 발표를 참고하였습니다.
이 포스트는 Lighthouse와 Chrome DevTool을 이용하여 퍼포먼스를 최적화하는 방법에 대해서 다루었습니다. 바닐라 자바스크립트 프로젝트에 직접 솔루션을 적용하였습니다.
불필요한 리소스 줄이기
브라우저에 보낼 웹페이지 리소스를 줄이기 위해, js와 css코드의 크기를 축소(minify)합니다. Minification(also called minimization)은 소스코드의 기능을 바꾸지 않고 소스코드에서 불필요한 characters를 제거하는 과정입니다. 여기서 불필요한 characters란 whitespace( ` ), newline(\n), comments(//,/,/), block delimeters({,}`) 등을 말합니다.
Minify JavaScript
위의 스크린샷과 같이 아무런 처리를 하지 않고 웹팩으로 빌드할 시, 번들링한 js파일의 용량은 큽니다(1.33MB).
var webpack = require("webpack");
module.exports = {
// ...
plugins: [
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
]
};
Network payload 줄이기(불필요한 다운로드 제거하기)
Lighthouse의 network payload audit에서 해당 페이지에서 요청한 모든 리소스의 크기(KiB)를 알 수 있습니다. HTTP Archive data에 따르면, 총 네트워크 요청 크기가 1700KiB에서 1900KiB 사이인 경우 보통 크기라고 합니다. Lighthouse의 경우 총 네트워크 요청 크기가 5000KiB를 넘는 경우 경고가 뜹니다. 참고로, 1600KiB 이하인 경우, 이론적으로 3G 네트워크에서 TTI(Time to Interactive)가 10초 이내로 나오게 됩니다.
Payload를 줄일 수 있는 방법은 다음과 같습니다:
리소스가 필요하기 전까지 요청을 뒤로 미룬다(defer). PRPL 패턴을 고려할 수 있다.
요청을 가능한 작게 최적화한다.
네트워크 페이로드를 축소(minify)하고 압축(compress)한다.
이미지를 JPEG나 PNG보다는 WebP를 사용한다.
JPEG 이미지 압축 레벨(compression level)을 85로 세팅한다.
요청을 캐시하여 페이지가 리소스를 재 다운로드하지 않도록 한다.
제 어플리케이션의 경우 805KiB로 flag가 뜨지 않았지만, 초기 페이지 로드 지연시간을 증가시키는 웹 폰트를 아래의 방법으로 최적화하였습니다.
웹 폰트 최적화하기
Google Font를 사용하면 페이지 로드 지연시간이 필연적으로 증가합니다. Lighthouse의 Opportunities audit에서도 Google Font와 관련하여 render-blocking resources를 제거할 시 460ms 만큼 페이지 로드 지연시간을 줄일 수 있다고 말하고 있습니다. render-blocking resources는 페이지가 Google Fonts 서버(fonts.googleapis.com)에서 CSS 파일을 fetch하기 이전까지 로드되지 못함을 의미합니다.
또한 Lighthouse의 Diagnostics audit을 보면 Avoid chaining critical requests을 권하고 있습니다. Critical request chain은 페이지 렌더링에서 우선순위가 높은 네트워크 요청 시리즈입니다. 이를 줄이기 위해 critical resources의 수를 줄이거나, 크기를 줄이거나, 순서를 최적화해야합니다.
먼저, 웹 폰트 다운로드 시간만큼 렌더링이 느려지므로<link rel="stylesheet" /> 대신 @font-face를 사용하여 폰트를 적용하겠습니다.
Resources are blocking the first paint of your page. Consider delivering critical JS/CSS inline [emphasis added] and deferring all non-critical JS/styles.
@font-face {
font-family: 'Noto Sans KR';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local("Noto Sans KR Regular"),
url(./assets/fonts/NotoSans/NotoSans-Regular.woff) format('woff');
}
@font-face {
font-family: 'Noto Sans KR';
font-style: normal;
font-weight: 700;
font-display: swap;
src: local("Noto Sans KR Bold"),
url(./assets/fonts/NotoSans/NotoSans-Bold.woff) format('woff');
}
웹폰트 확장자의 경우 대부분의 모던 브라우저에서 지원하는 WOFF를 사용했습니다. WOFF2의 경우 WOFF에 비해 지원률은 떨어지지만 압축률이 30% ~ 50%정도 더 좋습니다.
시스템에 폰트가 설치되어있다면 폰트 리소스를 요청하지 않도록 local 문법을 사용했습니다.
src: local("Noto Sans KR Regular"),
url(./assets/fonts/NotoSans/NotoSans-Regular.woff) format('woff');
JavaScript boot-up time은 js파일을 다운로드하고 (network), 자바스크립트 엔진에서 parse, compile, execute하는 시간을 모두 합한 지연시간을 말합니다.
Code Splitting
SPA(Single Page Application)은 초기 로딩 때 해당 웹앱의 모든 것을 다 불러와야하고, 이로 인해 초기 로딩시간이 길어진다는 단점이 있습니다.
이를 극복하기 위해 나온 해결책이 코드 스플리팅(code splitting)입니다. 해당 라우트를 방문했을 때만 관련된 모듈을 로딩하도록 하는 것입니다. 보통은 라우팅 부분에 코드 스플리팅을 구현하는 경우가 많습니다. React에서는 React.lazy와 dynamic import를 사용합니다.
웹팩 역시 코드 스플리팅을 지원합니다. 웹팩의 코드 스플리팅은 코드를 여러개의 번들로 쪼개 필요 시에 로드할 수 있게 합니다. 웹팩에서 코드 스플리팅 하는 방식은 보통 다음 세 가지 방식을 사용합니다.
Entry Points: entry 설정을 통해 쪼갠다.
Entry dependencies 혹은 SplitChunksPlugin을 사용해 청크 사이에 중복되는 부분을 삭제하고 청크를 쪼갠다.
dynamic imports: 모듈 내에서 함수 호출을 통해 쪼갠다.
dynamic imports
사용자가 보는 첫 페이지에서 필요하지 않은 코드는, 사용자가 특정 페이지나 위치에 도달할 때마다 로드하는 방식입니다. 첫 페이지 진입 시에 필요한 최소한의 코드만 다운받기 때문에 첫 페이지의 초기 성능을 향상시킬 수 있습니다.
런타임 시에 필요한 모듈을 동적으로 import합니다. 이런 방식을 lazy-load라고도 합니다. 다음과 같이 적용할 수 있습니다.
tree shaking은 나무를 흔들어 죽은 나뭇잎을 떨어뜨리듯, 코드를 빌드할 때 사용하지 않는 코드를 제거함으로써 용량을 줄이는 방식입니다. 특정 라이브러리를 참조하게 되면 참조한 라이브러리의 크기만큼 최종 번들의 크기가 증가합니다. 하지만 보통 특정 라이브러리의 모든 코드를 쓰지는 않습니다. 이 경우, 필요한 부분을 제외하고 제거할 수 있습니다.
먼저 tree shaking을 적용하기 위해선 require가 아닌 import/export문을 사용해야 합니다.
이 포스트는 Lighthouse와 Chrome DevTool을 이용하여 퍼포먼스를 최적화하는 방법에 대해서 다루었습니다. 바닐라 자바스크립트 프로젝트에 직접 솔루션을 적용하였습니다.
불필요한 리소스 줄이기
브라우저에 보낼 웹페이지 리소스를 줄이기 위해, js와 css코드의 크기를 축소(minify)합니다. Minification(also called minimization)은 소스코드의 기능을 바꾸지 않고 소스코드에서 불필요한 characters를 제거하는 과정입니다. 여기서 불필요한 characters란 whitespace(
` ), newline(
\n), comments(
//,
/,
/), block delimeters(
{,
}`) 등을 말합니다.위의 스크린샷과 같이 아무런 처리를 하지 않고 웹팩으로 빌드할 시, 번들링한 js파일의 용량은 큽니다(1.33MB).
webpack5에서 권장하는 terser-webpack-plugin을 사용해보겠습니다.
아래 스크린샷을 보면, js파일 용량이 1.33MB에서 1.23MB로 줄어든 것을 확인할 수 있습니다.
MiniCssExtractPlugin은 CSS를 style 태그가 아닌 별도의 css 파일로 만들어줍니다.
사용하지 않는 JavaScript, CSS 코드 제거하기
코드 커버리지는 테스트 시 실행된 코드의 비율을 말합니다. 코드 커버리지를 높이면 장기적인 관점에서 코드 결함을 감소시킨다는 경험적인 결론이 있습니다.
Chrome DevTools의 Coverage탭에서 CSS와 JS 코드의 코드 커버리지를 분석할 수 있습니다. 사용하지 않는 코드를 제거하면 페이지 로드 속도를 높일 수 있으며 모바일 유저의 셀룰러 데이터 사용량을 줄일 수 있습니다.
app.css
파일의 경우, code coverage를 참고하여 사용되지 않는 css 선택자를 제거합니다.어플리케이션에서 사용하고 있는 chart.js 라이브러리는 moment.js 라이브러리에 의존하고 있습니다 (moment.locale 250k).
[IgnorePlugin
을 이용해 모듈 생성을 방지하였습니다.](https://stackoverflow.com/questions/25384360/how-to-prevent-moment-js-from-loading-locales-with-webpack)Network payload 줄이기(불필요한 다운로드 제거하기)
Lighthouse의 network payload audit에서 해당 페이지에서 요청한 모든 리소스의 크기(KiB)를 알 수 있습니다. HTTP Archive data에 따르면, 총 네트워크 요청 크기가 1700KiB에서 1900KiB 사이인 경우 보통 크기라고 합니다. Lighthouse의 경우 총 네트워크 요청 크기가 5000KiB를 넘는 경우 경고가 뜹니다. 참고로, 1600KiB 이하인 경우, 이론적으로 3G 네트워크에서 TTI(Time to Interactive)가 10초 이내로 나오게 됩니다.
Payload를 줄일 수 있는 방법은 다음과 같습니다:
제 어플리케이션의 경우 805KiB로 flag가 뜨지 않았지만, 초기 페이지 로드 지연시간을 증가시키는 웹 폰트를 아래의 방법으로 최적화하였습니다.
웹 폰트 최적화하기
Google Font를 사용하면 페이지 로드 지연시간이 필연적으로 증가합니다. Lighthouse의 Opportunities audit에서도 Google Font와 관련하여 render-blocking resources를 제거할 시 460ms 만큼 페이지 로드 지연시간을 줄일 수 있다고 말하고 있습니다. render-blocking resources는 페이지가 Google Fonts 서버(fonts.googleapis.com)에서 CSS 파일을 fetch하기 이전까지 로드되지 못함을 의미합니다.
또한 Lighthouse의 Diagnostics audit을 보면 Avoid chaining critical requests을 권하고 있습니다. Critical request chain은 페이지 렌더링에서 우선순위가 높은 네트워크 요청 시리즈입니다. 이를 줄이기 위해 critical resources의 수를 줄이거나, 크기를 줄이거나, 순서를 최적화해야합니다.
먼저, 웹 폰트 다운로드 시간만큼 렌더링이 느려지므로
<link rel="stylesheet" />
대신@font-face
를 사용하여 폰트를 적용하겠습니다.참고: python을 활용한 subset 생성기
JavaScript Boot-up time 줄이기
JavaScript boot-up time은 js파일을 다운로드하고 (network), 자바스크립트 엔진에서 parse, compile, execute하는 시간을 모두 합한 지연시간을 말합니다.
Code Splitting
SPA(Single Page Application)은 초기 로딩 때 해당 웹앱의 모든 것을 다 불러와야하고, 이로 인해 초기 로딩시간이 길어진다는 단점이 있습니다.
이를 극복하기 위해 나온 해결책이 코드 스플리팅(code splitting)입니다. 해당 라우트를 방문했을 때만 관련된 모듈을 로딩하도록 하는 것입니다. 보통은 라우팅 부분에 코드 스플리팅을 구현하는 경우가 많습니다. React에서는
React.lazy
와 dynamicimport
를 사용합니다.웹팩 역시 코드 스플리팅을 지원합니다. 웹팩의 코드 스플리팅은 코드를 여러개의 번들로 쪼개 필요 시에 로드할 수 있게 합니다. 웹팩에서 코드 스플리팅 하는 방식은 보통 다음 세 가지 방식을 사용합니다.
entry
설정을 통해 쪼갠다.SplitChunksPlugin
을 사용해 청크 사이에 중복되는 부분을 삭제하고 청크를 쪼갠다.dynamic imports
사용자가 보는 첫 페이지에서 필요하지 않은 코드는, 사용자가 특정 페이지나 위치에 도달할 때마다 로드하는 방식입니다. 첫 페이지 진입 시에 필요한 최소한의 코드만 다운받기 때문에 첫 페이지의 초기 성능을 향상시킬 수 있습니다.
런타임 시에 필요한 모듈을 동적으로 import합니다. 이런 방식을 lazy-load라고도 합니다. 다음과 같이 적용할 수 있습니다.
Tree Shaking
tree shaking은 나무를 흔들어 죽은 나뭇잎을 떨어뜨리듯, 코드를 빌드할 때 사용하지 않는 코드를 제거함으로써 용량을 줄이는 방식입니다. 특정 라이브러리를 참조하게 되면 참조한 라이브러리의 크기만큼 최종 번들의 크기가 증가합니다. 하지만 보통 특정 라이브러리의 모든 코드를 쓰지는 않습니다. 이 경우, 필요한 부분을 제외하고 제거할 수 있습니다.
먼저 tree shaking을 적용하기 위해선 require가 아닌 import/export문을 사용해야 합니다.
이를 위해 babel 설정을 아래와 같이
modules: false
로 지정하였습니다. modules를 false로 하면 import, export가 require, module.exports로 바뀌지 않습니다.실제 사용하고 있는 코드만 빌드하기 위해 import하고 있는 chart.js 라이브러리 모듈에서 제가 사용할 부분만 import하였습니다 (Chart.js 3부터 가능합니다). 목록은 공식 홈페이지에서 참고하였습니다.
위 코드처럼 제가 사용할 bar chart와 관련된 것들만 import하였습니다.
트리 쉐이킹 결과 번들의 크기가 약 20KiB 정도 줄어든 것을 확인할 수 있습니다.
참고자료
https://developers.google.com/web/updates/2018/05/lighthouse
https://blog.logrocket.com/terser-vs-uglify-vs-babel-minify-comparing-javascript-minifiers/
https://testing.googleblog.com/2020/08/code-coverage-best-practices.html?m=1
https://developer.chrome.com/docs/devtools/coverage/
https://web.dev/total-byte-weight/
https://webpack.js.org/guides/code-splitting/
https://medium.com/hackernoon/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758
https://mariusschulz.com/blog/dynamic-import-expressions-in-typescript
https://www.zerocho.com/category/Webpack/post/58ad4c9d1136440018ba44e7
https://velog.io/@vnthf/웹폰트-최적화-하기