ReactMasters / study

스터디 기록
1 stars 0 forks source link

11월 3주차 Lighthouse를 이용해 퍼포먼스 최적화하기 #34

Open jordan-choi opened 2 years ago

jordan-choi commented 2 years ago
[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({,}`) 등을 말합니다.

  1. Minify JavaScript 1

위의 스크린샷과 같이 아무런 처리를 하지 않고 웹팩으로 빌드할 시, 번들링한 js파일의 용량은 큽니다(1.33MB).

webpack5에서 권장하는 terser-webpack-plugin을 사용해보겠습니다.

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin({ ... })],
  },
};

아래 스크린샷을 보면, js파일 용량이 1.33MB에서 1.23MB로 줄어든 것을 확인할 수 있습니다.

2

  1. Minify CSS

MiniCssExtractPlugin은 CSS를 style 태그가 아닌 별도의 css 파일로 만들어줍니다.

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  plugins: [new MiniCssExtractPlugin({
      linkType: false,
    filename: '[name].css',
  })],
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
};

3

사용하지 않는 JavaScript, CSS 코드 제거하기

코드 커버리지는 테스트 시 실행된 코드의 비율을 말합니다. 코드 커버리지를 높이면 장기적인 관점에서 코드 결함을 감소시킨다는 경험적인 결론이 있습니다.

Chrome DevTools의 Coverage탭에서 CSS와 JS 코드의 코드 커버리지를 분석할 수 있습니다. 사용하지 않는 코드를 제거하면 페이지 로드 속도를 높일 수 있으며 모바일 유저의 셀룰러 데이터 사용량을 줄일 수 있습니다.

4

5

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)

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를 줄일 수 있는 방법은 다음과 같습니다:

제 어플리케이션의 경우 805KiB로 flag가 뜨지 않았지만, 초기 페이지 로드 지연시간을 증가시키는 웹 폰트를 아래의 방법으로 최적화하였습니다.

웹 폰트 최적화하기

Google Font를 사용하면 페이지 로드 지연시간이 필연적으로 증가합니다. Lighthouse의 Opportunities audit에서도 Google Font와 관련하여 render-blocking resources를 제거할 시 460ms 만큼 페이지 로드 지연시간을 줄일 수 있다고 말하고 있습니다. render-blocking resources는 페이지가 Google Fonts 서버(fonts.googleapis.com)에서 CSS 파일을 fetch하기 이전까지 로드되지 못함을 의미합니다.

6

또한 Lighthouse의 Diagnostics audit을 보면 Avoid chaining critical requests을 권하고 있습니다. Critical request chain은 페이지 렌더링에서 우선순위가 높은 네트워크 요청 시리즈입니다. 이를 줄이기 위해 critical resources의 수를 줄이거나, 크기를 줄이거나, 순서를 최적화해야합니다.

7

@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');
}

폰트 형식에 따른 브라우저 지원 범위

src: local("Noto Sans KR Regular"),
        url(./assets/fonts/NotoSans/NotoSans-Regular.woff) format('woff');

참고: python을 활용한 subset 생성기

@font-face {
    font-display: swap;
}

웹폰트 최적화 결과

JavaScript Boot-up time 줄이기

JavaScript boot-up time은 js파일을 다운로드하고 (network), 자바스크립트 엔진에서 parse, compile, execute하는 시간을 모두 합한 지연시간을 말합니다.

JavaScript processing([https://developers.google.com/web/updates/2018/08/web-performance-made-easy#lower_javascript_boot-up_time_with_code_splitting](https://developers.google.com/web/updates/2018/08/web-performance-made-easy#lower_javascript_boot-up_time_with_code_splitting))

Code Splitting

SPA(Single Page Application)은 초기 로딩 때 해당 웹앱의 모든 것을 다 불러와야하고, 이로 인해 초기 로딩시간이 길어진다는 단점이 있습니다.

이를 극복하기 위해 나온 해결책이 코드 스플리팅(code splitting)입니다. 해당 라우트를 방문했을 때만 관련된 모듈을 로딩하도록 하는 것입니다. 보통은 라우팅 부분에 코드 스플리팅을 구현하는 경우가 많습니다. React에서는 React.lazy와 dynamic import를 사용합니다.

웹팩 역시 코드 스플리팅을 지원합니다. 웹팩의 코드 스플리팅은 코드를 여러개의 번들로 쪼개 필요 시에 로드할 수 있게 합니다. 웹팩에서 코드 스플리팅 하는 방식은 보통 다음 세 가지 방식을 사용합니다.

dynamic imports

사용자가 보는 첫 페이지에서 필요하지 않은 코드는, 사용자가 특정 페이지나 위치에 도달할 때마다 로드하는 방식입니다. 첫 페이지 진입 시에 필요한 최소한의 코드만 다운받기 때문에 첫 페이지의 초기 성능을 향상시킬 수 있습니다.

런타임 시에 필요한 모듈을 동적으로 import합니다. 이런 방식을 lazy-load라고도 합니다. 다음과 같이 적용할 수 있습니다.

import('utils/chart').then(({ addChart }) => {
    const newChart = addChart('bar', store._state.covidData, chart.chartOptions.period, chart.chartOptions.category, context);
  this._barCharts.push(newChart);
}); 

Tree Shaking

tree shaking은 나무를 흔들어 죽은 나뭇잎을 떨어뜨리듯, 코드를 빌드할 때 사용하지 않는 코드를 제거함으로써 용량을 줄이는 방식입니다. 특정 라이브러리를 참조하게 되면 참조한 라이브러리의 크기만큼 최종 번들의 크기가 증가합니다. 하지만 보통 특정 라이브러리의 모든 코드를 쓰지는 않습니다. 이 경우, 필요한 부분을 제외하고 제거할 수 있습니다.

먼저 tree shaking을 적용하기 위해선 require가 아닌 import/export문을 사용해야 합니다.

Tree shaking relies on the static structure of ES2015 module syntax, i.e., import and export. (https://webpack.js.org/guides/tree-shaking/)

이를 위해 babel 설정을 아래와 같이 modules: false로 지정하였습니다. modules를 false로 하면 import, export가 require, module.exports로 바뀌지 않습니다.

module.exports = {
    presets: [
      [
        '@babel/preset-typescript',
        {
          modules: false,
          ...
        },
      ],
    ],
}

실제 사용하고 있는 코드만 빌드하기 위해 import하고 있는 chart.js 라이브러리 모듈에서 제가 사용할 부분만 import하였습니다 (Chart.js 3부터 가능합니다). 목록은 공식 홈페이지에서 참고하였습니다.

import { Chart, BarElement, BarController, LinearScale, CategoryScale, ChartType } from 'chart.js';

Chart.register(
    BarElement,
    BarController,
    LinearScale,
    CategoryScale,
);

위 코드처럼 제가 사용할 bar chart와 관련된 것들만 import하였습니다.

트리 쉐이킹 결과 번들의 크기가 약 20KiB 정도 줄어든 것을 확인할 수 있습니다.

assets by info 334 KiB [immutable]
  assets by path *.js 334 KiB
    asset vendors.267481ca17f0c981c48f.bundle.js 302 KiB [emitted] [immutable] [minimized] [big] (name: vendors) (id hint: vendor) 1 related asset
    asset app.c6e8e89ac44947ec0964.bundle.js 19.4 KiB [emitted] [immutable] [minimized] (name: app)
    asset runtime.46053b539ab1b91ce632.bundle.js 12.3 KiB [emitted] [immutable] [minimized] (name: runtime)
  assets by chunk 141 bytes (auxiliary name: app)
    asset 158c3f3e39ef2f860f3c.woff 72 bytes [emitted] [immutable] [from: src/assets/fonts/NotoSans/NotoSans-Regular.woff] (auxiliary name: app)
    asset 1fd4142f8fb913351a8c.woff 69 bytes [emitted] [immutable] [from: src/assets/fonts/NotoSans/NotoSans-Bold.woff] (auxiliary name: app)
assets by path assets/*.woff 463 KiB
  asset assets/NotoSans-Bold.woff 233 KiB [emitted] [from: src/assets/fonts/NotoSans/NotoSans-Bold.woff] (auxiliary name: app)
  asset assets/NotoSans-Regular.woff 230 KiB [emitted] [from: src/assets/fonts/NotoSans/NotoSans-Regular.woff] (auxiliary name: app)
asset app.css 8.88 KiB [emitted] (name: app) 1 related asset
asset index.html 1.18 KiB [emitted]
assets by info 313 KiB [immutable]
  assets by path *.js 313 KiB
    asset vendors.83598341ee758810c2b1.bundle.js 281 KiB [emitted] [immutable] [minimized] [big] (name: vendors) (id hint: vendor) 1 related asset
    asset app.06d5a2e8941dc8692f19.bundle.js 19.4 KiB [emitted] [immutable] [minimized] (name: app)
    asset runtime.670ef18af166a7340e88.bundle.js 12.3 KiB [emitted] [immutable] [minimized] (name: runtime)
  assets by chunk 141 bytes (auxiliary name: app)
    asset 158c3f3e39ef2f860f3c.woff 72 bytes [emitted] [immutable] [from: src/assets/fonts/NotoSans/NotoSans-Regular.woff] (auxiliary name: app)
    asset 1fd4142f8fb913351a8c.woff 69 bytes [emitted] [immutable] [from: src/assets/fonts/NotoSans/NotoSans-Bold.woff] (auxiliary name: app)
assets by path assets/*.woff 463 KiB
  asset assets/NotoSans-Bold.woff 233 KiB [emitted] [from: src/assets/fonts/NotoSans/NotoSans-Bold.woff] (auxiliary name: app)
  asset assets/NotoSans-Regular.woff 230 KiB [emitted] [from: src/assets/fonts/NotoSans/NotoSans-Regular.woff] (auxiliary name: app)
asset app.css 8.88 KiB [emitted] (name: app) 1 related asset
asset index.html 1.18 KiB [emitted]

참고자료

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/웹폰트-최적화-하기

jordan-choi commented 2 years ago

tree shaking example