우테코

프론트엔드 성능 최적화 - 1

prefer2 2022. 9. 6. 17:24

 

 

기본 설정


강력 새로고침(cmd+shift+r)을 매번 사용하지 않고 사이트에 처음 방문한 것 과 같은 환경을 유지하기 위해 Chorme의 Network의 캐시사용 중지를 활성화시켜주었다.

 

 

측정하기


성능을 개선하기 위해서는 어떤 점을 개선해야 할 지 찾아야 한다. 이를 위해 기존의 사이트의 성능을 측정해보았다

 

lighthouse

Chrome의 개발자도구에 들어가면 Lighthouse 탭을 확인할 수 있다. 원하는 조건을 선택하여 성능을 측정해보자. memegle은 데스크탑에서 주로 사용될 것이라 여겨져 데스크탑 기준으로 성능을 측정하였다.

성능, 접근성, 권장사항, SEO, PWA에 대해 측정이 가능하다. 이 값은 어디서 측정하는지 그리고 어떤 기기를 사용하는지에 따라 매번 측정 결과가 다르게 나온다. 가장 정확한 성능 측정을 위해서는 secret mode 사용을 하는 것이 좋다. 이 중 이번에 집중하고자 하는 부분은 성능 부분이다. 

비교할 수 있는 초기값을 가지고 있으니 이제 성능을 최적화시켜보자!

 

 

요청의 크기 줄이기


어떤 사이트에 접속하게 될때 서버에게 이 사이트에 대한 내용들을 보내달라고 요청하게 된다. 만약 이 요청들의 크기가 크다면, 요청을 받아오는데 많은 시간이 걸리게 되고, 결과적으로 유저에게 느리게 보이게 된다. 서버에게 요청을 하게 되는 이런 정적 파일들의 크기를 줄여보자.

 

소스코드 크기 줄이기

webpack설정을 통하여 빌드시 생성되는 js파일의 크기를 줄여보자. webpack4 버전부터 추가된 mode를 설정하면 production, development 모드에 따라서 코드를 최적화해준다(참고). mode를 설정하지 않으면 production 모드로 기본 설정이 되어 있다. 웹팩에서는 모드에 따라 파일을 나누는 것을 권장하고 있지만, 이번에는 성능 최적화가 목적이기 때문에 하나의 파일에서 production모드로 진행했다.

 

자바스크립트 압축과 난독화

압축화 - 모든 들여쓰기와 공백이 제거되고, 전체 코드가 한 줄로 병합된다. 원본 코드에서 들여쓰기, 공백, 콤마 등이 제대로 사용되지 않았다면 압축된 코드에서 문제가 생길 수 있다.

난독화 - 자바스크립트 코드 자체를 분석하기 어렵게 만드는 과정. 변수, 함수명 등이 줄어 용량 감소. 하지만 난독화 단계가 높을수록 코드를 해석하고 실행하는 속도가 느려질 수 있다.

webpack4 이상에서는 production모드에서 자동으로 압축과 난독화를 진행해준다(참고). webpack5 부터는 TerserPlugin이 내장되어 있어 따로 설치를 해주지 않아도 된다. TerserPlugin 관련 추가 설정을 해주고 싶은 경우에는 설치해서 설정을 해주면 된다.

 

source-map

sourceMap은 개발하는 코드와 번들링된 코드 사이의 관계를 표현하는 데이터이다. 번들링된 코드로부터 소스 코드를 유추하는 것은 비효율적이기 때문에 개발하는 코드와 번들링된 코드를 연결하는 sourceMap이 필요하다.

webpack의 devtool로 어떤 방식으로 sourceMap을 생성할 것인지 설정할 수 있다. 공식문서에서 production 모드에게 추천하는데로 옵션을 넣지 않았다(참고). 이렇게하면 .map관련 파일들이 생성되지 않는다.

 

css 파일 분리

css 파일또한 js파일처럼 압축이 가능하다. 뿐만 아니라 한번에 번들되어 있는 css 파일을 분리 할 수 있다. MiniCssExtractPlugin을 사용하여 번들되어있는 css 파일을 분리해보자. 설정의 경우 공식 문서에서 제안하는 방법과 동일하게 해주었다.

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

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

이 과정에서 들었던 의문은 과연 css 파일을 분리해주는 것이 성능상에 더 유리한가였다. css 파일을 분리하면 번들 파일의 크기가 작아진다는 이점이 있지만, css 파일 요청이 한 번 더 일어난다. 프로젝트에서 css의 비중, 번들링 개수에 따라 적절하게 선택하는 것이 좋겠다는 생각이 들었다. 

compress( a + b) ≤ compress(a) + compress(b)

하나의 큰 파일을 압축하는 것이 작은 파일 여러개를 압축하는 것보다 좋은 결과를 낼 수 있다. 따라서 어떤 코드에서는 chunk를 나누지 않는 것이 download를 최적화하고 browser 성능을 위해서는 더 좋을 수 도 있다(참고)

실험 결과 이번 미션에서는 css파일을 분리하는 것보다 함께 번들링하는 것이 좋은 것 같아 MiniCssExtractPlugin은 사용하지 않았다.

 

이미지 크기 줄이기

초기에는 webpack설정을 통해 build시 png->webp로 확장자명을 바꿔주는 방식을 생각했다. 이후 webpack의 ImageMinimizerWebpackPlugin을 통해 다시 한 번 압축을 해주면 이미지 크기가 획기적으로 준다.

하지만 이 방법의 경우 webp를 제공하지 않는 구형 브라우저에서는 이미지를 보지 못한다는 큰 단점이 있다.

{
    test: /\.(png)$/i,
    type: 'asset',
    generator: {
      filename: 'static/[name].webp[query]'
    }
}
new ImageMinimizerPlugin({
        minimizer: {
          implementation: ImageMinimizerPlugin.imageminGenerate,
          options: {
            plugins: [
              ['gifsicle', { interlaced: true, optimizationLevel: 3, colors: 256 }],
              ['pngquant', { quality: [0.4, 0.6] }],
              ['webp', { quality: 50, resize: { width: 1280, height: 0 } }]
            ]
          }
        }
      })

 

png -> webp

webp는 jpg, png대비 약 30% 작은 용량의 파일로 줄일 수 있지만, 이미지 퀄리티는 동일하다는 장점이 있다. 다만 이를 지원하지 않는 브라우저가 꽤나 있다.

다양한 브라우저에서 사용할 수 있도록 <picture>, <source> 태그를 사용하기로 했다. 또한 srcSet을 사용해서 기기마다 다른 사이즈의 이미지를 제공하도록 했다. 여기서 w은 pixel수를 의미한다.

<picture>
	<source
    	type="image/webp"
        src={heroImageDesktop}
        srcSet={`${heroImageMobile} 375w, ${heroImageTablet} 768w, ${heroImageDesktop} 1980w`}
      />
	<img className={styles.heroImage} src={heroImage} alt="hero image" />
</picture>

 

위 코드가 잘 동작하는지 확인해보자. Chrome 개발자 도구에서 Rendering에 있는 WebP 이미지 형식 사용 중지를 체크해준다.

 

참고자료: https://web.dev/serve-images-webp/

 

gif -> mp4

빠른 로드를 위해 gif파일을 mp4로 변환하였다. gif처럼 보이도록 하기 위해 video 태그의 속성들을 설정해주었다. webM 형식으로 할 시 webP와 동일하게 다양한 브라우저에서 지원할 수 있도록 mp4형식과 함께 넣어주어야 한다.

<video autoplay loop muted playsinline></video>

 

참고자료: https://web.dev/replace-gifs-with-videos/

 

 

필요한 것만 요청하기


코드를 작성하고 나면 이전에 작성했던 불필요한 코드나 실제로 사용되지 않는 코드들이 포함되어 있을 수 있다. 또한 현재 화면에서는 필요하지 않는 값들을 미리 가져오는 것은 낭비일 수 있다. 불필요한 값들을 제거하고 사용하는 값들만 불러오도록 수정해보자.

 

code-splitting

하나의 파일로 번들링을 하면 페이지에서 사용하지 않는 모듈들까지 들어가 파일이 커질 수 있다. 번들이 거대해지는 것을 방지하기 위한 좋은 해결방법은 번들을 나누는 것이다.  webpack에서 chunck를 나누거나, dynamic import를 통해 code spliiting을 할 수 있다.

React에서는 Suspense와 lazy를 통해 code splitting을 할 수 있다. 가장 기본적으로 적용해 볼 수 있는 것은 라우트별로 lazy를 걸어두는 것이다. 이렇게 하면 원하는 페이지만 우선적으로 불러올 수 있다.

 

tree shaking

불필요한 리소스 요청이 있는지 확인하고 사용하지 않는 값들이 있다면 제거해준다. 모듈이 es6의 import, export를 사용해야 tree shaking이 된다. webpack을 통해서 tree shaking이 자동으로 적용되지만 잘 되지 않는다면 commonJS로 작성된 것이 아닌가 의심해볼 필요가 있다. 또한 작성한 코드를 tree shaking하고 싶다면 es6 문법을 es5로 바꿔주는 babel의 설정을 해주어야 한다.

"presets": [
    ["env", {
      "modules": false
    }]
  ]

 

브라우저 리소스 우선순위 정하기

- preload

현재 페이지에서 바로 필요한 리소스로, 빠르게 가져와야 한다는 것을 브라우저에게 알려주는 것이다. 우리는 중요한 리소스가 무엇인지 알고 있다. 이러한 리소스를 미리 요청하면 로딩 프로세스의 속도를 높일 수 있다. 헤드에 rel="preload"가 있는 <link>태그를 추가하면 원하는 리소스를 미리 로드할 수 있고, 브라우저는 이 리소스를 캐시하므로 필요 시 즉시 사용할 수 있다

값을 fetch해온 후 바로 실행하지 않는다. onload 이후 3초 이내에 preload한 값을 사용하지 않으면 크롬 콘솔에 경고가 뜨게 된다. 폰트나 초기 렌더링에 반드시 필요한 리소스들에게 적용할 수 있다.

<head>
  <link rel="preload" as="script" href="critical.js">
</head>

 

참고자료: https://web.dev/preload-critical-assets/

 

- prefetch

유저가 미래에 사용할 페이지의 리소스들을 미리 fetch해온다. 사용자의 흐름을 예측할 수 있을 때 사용하면 좋다. FCP(First Contentful Paint)나 TTI(Time to Interactive)을 향상시킬 수 있다. 

가장 간단한 방법은 방문하는 페이지의 첫번째 링크나 몇몇 링크들을 prefetch하는 것이다. 예를들어 ebay는 검색 결과의 상위 5개 항목을 prefetch해온다.

code split을 적용한 bundle에도 prefetch를 할 수 있다. 곧 사용할 페이지나 컴포넌트에 prefetch를 적용할 수 있다. 

<head>
	...
	<link rel="prefetch" href="/articles/" as="document">
	...
</head>

어떤 값을 prefetch하는지 미리 암시하여 브라우저가 리소스를 파싱하기 전에 리소스 힌트를 제공할 수 있도록 한다.

🚨주의: 대부분의 현대 브라우저에서 prefetch를 지원하지만 safari(제2의 IE)에서는 prefetch를 지원하지 않는다. XHR request또는 Fetch API를 사용하여 대비하자.

 

webpack 사용하여 javascript module prefetch하기

form.addEventListener("submit", e => {
   e.preventDefault()
   import(/* webpackPrefetch: true */ 'lodash.sortby')
         .then(module => module.default)
         .then(sortInput())
         .catch(err => { alert(err) });
});

webpack의 magic comment를 사용하여 prefetch할 수 있다. 위 코드는 아래의 코드를 index.html에 주입하라고 알려준다.

<link rel="prefetch" as="script" href="1.bundle.js">

 

참고자료: https://web.dev/link-prefetch/

 

 

반응형

'우테코' 카테고리의 다른 글

Refresh Token 도입기  (0) 2022.10.23
프론트엔드 성능 최적화 - 2  (2) 2022.09.12
내편 UI 개발기  (6) 2022.09.02
[Level 2] 미션 1: 페이먼츠 1, 2단계  (0) 2022.05.16
[Level 1] 미션 4: 자판기 미션 1단계  (0) 2022.04.01