발표
4주차 발표
김호준

https://github.com/performance-lecture (opens in a new tab)

프론트엔드 성능 최적화 하기

성능 최적화가 중요한 이유

  • 페이지 표시 시간이 1초에서 3초로 느려진 경우, 사용자 이탈률 32프로 증가, 1초에서 5초는 사용자 이탈률 90프로 증가

  • 핀터레스트의 경우 로딩 시간을 40프로 줄임으로써 가입자 수를 15프로 늘렸따.

웹 성능을 결정하는 요소

  • 로딩 성능

    서버에 있는 웹 페이지와 기타 리소스를 다운로드할 때의 성능 (HTML, CSS, 자바스크립트 파일)

  • 렌더링 성능

    다운로드한 리소스를 가지고 화면을 그릴 때의 성능(코드를 얼마나 효율적으로 작성했느냐에 따라 화면이 그려지는 속도와 사용자 인터랙션의 자연스러운 정도)

개발자도구에 대해서 간단하게

네트워크 탭

퍼포먼스 패널

  • 웹 페이지가 로드될 때, 실행되는 모든 작업을 보여줌
  • 브라우저 메인스레드에서 실행되는 자바스크립트를 차트 형태로 볼 수 있다.

라이트하우스

  • 웹사이트의 성능을 측정해주는 툴
  • mode, device, category 적절하게 선택하면 된다.
    • Navigation은 초기 페이지 로딩 시 발생하는 성능 문제
    • Timespan은 사용자가 정의한 시간 동안 발생한 성능 문제를 분석
    • Snapshot은 현재 상태의 성능 문제를 분석
  • performance는 로딩 과정에서의 성능 문제 분석
  • Accessibiliy는 접근성 문제 분석
  • Best Practice 웹사이트의 보안문제, 웹 개발의 최신 표준
  • SEO 검색 엔진에서 얼마나 잘 크롤링되는지
  • PWA PWA와 관련된 문제를 분석 (서비스워커와 오프라인 동작 등)

라이트하우스 검사 후에

  • LCP : 화면 내에 있는 가장 큰 이미지나 텍스트 요소가 렌더링되기까지 걸리는 시간
  • TTI : 사용자가 페이지와 상호 작용이 가능한 시점까지 걸리는 시간
  • TBT : 페이지가 클릭, 키보드 입력 등의 사용자 입력에 응답하지 않도록 차단된 시간을 총합한 지표
  • CLS : 페이지 로드 과정에서 발생하는 예기치 못한 레이아웃 이동을 측정한 지표
  • SI : 페이지 로드 중에 콘텐츠가 시각적으로 표시되는 속도 (스켈레톤이 있냐 없냐에 차이)
  • FCP : 페이지에 진입하여 첫 콘텐츠가 뜨기까지 걸리는 시간

최적화 기법

이미지 사이즈 최적화

  • 다양한 이미지를 상황에 맞게 적절하게 사용해서 성능을 최적화 하자.

    Elements 패널에서 img 요소를 찾아서 src 값 위에 커서를 올리면 img에 대한 정보를 확인할 수 있다. 렌더 사이즈와 실제 이미지 사이즈를 비교할 수 있다. 적절한 사이즈는 레티나 디스플레이에 대응하기 위해서 렌더 사이즈의 x2의 크기로 최적화하는 것이 좋다.

    • CDN을 쓰는 경우 보통 쿼리스트링으로 가로 세로 조절 가능
    • 클라우디너리의 경우
    /image/w_400,h_500,c_fill,q_auto:best/~

코드 분할

리액트 코드가 하나의 자바스크립트 파일로 번들링되어 로드되는데 첫 페이지 진입 시 당장 사용하지 않는 코드를 분리하고 해당 코드가 필요한 시점에 따로 로드하는 방법

  • webPack Bundle Analyzer
npm install --save-dev cra-bundle-analyzer
npx cra-bundle-analyzer

refractor package-lock.json에서 찾아서 참조하고 있는 react syntax 패키지를 찾아볼 수 있다. 이 패키지는 블로그 글 상세 페이지에서만 사용되는데 처음 진입하는 목록 페이지에서는 패키지를 굳이 다운로드 할 필요가 없음

  • Dynamic Import

    app.js 에서 컴포넌트들을 lazy함수로 동적 import해오고 route부분을 suspense로 감싸고 컴포넌트에 넣어주면 각 페이지 컴포넌트의 코드가 분할 된다.

import React, { Suspense } from "react";
import { Switch, Route } from "react-router-dom";
import "./App.css";
import ListPage from "./pages/ListPage/index";
import ViewPage from "./pages/ViewPage/index";
 
function App() {
  return (
    <div className="App">
      <Suspense fallback={<div>...로딩중</div>}>
        <Switch>
          <Route path="/" component={ListPage} exact />
          <Route path="/view/:id" component={ViewPage} exact />
        </Switch>
      </Suspense>
    </div>
  );
}
 
export default App;
import("add").then((module) => {
  const { add } = module;
 
  console.log("1+4=", add(1, 4));
});

webPack은 동적 import 구문을 만나면 코드를 분할하여 번들링한다. -> Promise형태로 모듈을 반환하게 된다. 컴포넌트를 import하려면 프로미스는 문제가 생길 수 있음, 프로미스 밖으로 빼줘야 한다. Lazy, Suspense 사용함

  • Lazy
const ListPage = lazy(() => import("컴포넌트 주소"));
  • Suspense
<Suspense>
  <Component />
</Suspense>

지연 로딩의 단점

  • 지연 로딩 기법을 적용하면 최초 페이지를 로드할 때 당장 필요 없는 모달과 관련된 코드가 번들에 포함되지 않아, 로드할 파일의 크기가 작아지고 초기 로딩 속도나 자바스크립트의 실행 타이밍이 빨라져서 화면이 더 발리 표시된다는 장점이 있다.
  • 이 기법은 모달과 같은 컴포넌트를 사용할 때는 한계점이 있다. 모달을 띄울 때 네트워크를 통해 코드를 새로 로드해야 하며 로드가 완료되어야만 모달을 띄울 수 있기 때문이다.
  • 이때는 사전로딩 기법을 이용할 수 있다. -> 사전 로딩 부분으로

텍스트를 압축

  • 다운로드 전에 서버에서 미리 압축할 수 있는데, 그러면 원래 사이즈보다 더 작은 사이즈로 다운로드 할 수 있어 웹 페이지를 더 빠르게 로드할 수 있다.

  • 라이트하우스 텍스트 압축(Enable text compression)

    예시 같은 경우에는 server라이브러리라는 서버를 사용하고 있음. 실제 서비스에서는 nginx, apache같은 곳에서 텍스트 압축을 적용해야 함.

병목 코드 최적화

코드를 비효율적으로 짜게 되면 해당 코드때문에 서비스의 성능이 낮아질 수 있음. 이때 어떤 코드가 무엇 때문에 느린지 몰라서 문제를 파악하고 해결하기 어려워지는데 이런 코드를 어떻게 찾는지(이러한 코드를 병목코드라고 한다.)

  • Reduce unused JavaScript 항목

  • 퍼포먼스 패널

    • 씨피유 차트 : 자바스크립트는 노란색, 렌더링/레이아웃은 보라색, 페인팅은 초록색, 기타 시스템 작업은 회ㅐㄱ
    • 프레임 : 스크린샷으로 화면 변화를 찍음
    • 하단 Summary : 발생한 작업 시간의 총합과 각 작업이 차지하는 비중
    • 바텀, 콜트리는 : 상하위 작업 순으로 보여줌
    • 이벤트 로그 : 발생한 이벤트를 보여줌

병목 코드 찾기

  • 퍼포먼스 탭에 보면 메인스레드에 chunk.js가 끝난 시점에 ParseHTMl이 시작된다. 끝난 후에 네트워크 탭을 보면 articles 컴포넌트에서 블로그 글 데이터를 요청하는 걸 볼 수 있다.
  • 타이밍 섹션에 보면 아티클 컴포넌트의 실행 시간이 긴 걸 볼 수 있다.
  • 메인 스레드에서 해당 구간을 다시 보면 리무브 스페셜 차트 js함수가 아주 오래 실행되었다를 볼 수 있음.
  • 이 함수는 제거할 특수문자를 정의해두고, 각 특수 문자마다 반복문을 돌려서 본문에 일치하는 내용을 탐색하고 제거하는데 반복문을 두 번 돌리게 된다. -> 비효율적임
  • substring, concat -> replace함수로 대체

애니메이션 최적화하기

브라우저에 렌더링 과정 이해하기

DOM + CSSOM -> 렌더 트리 -> 레이아웃 -> 페인트 -> 컴포지트

이러한 과정을 픽셀 파이프라인 이라고한다.

DOM, CSSOM : HTML, CSS 등 화면을 그리는데 필요한 리소스를 다운로드하고 파싱해서 DOM을 만든다.

마찬가지로 CSS도 CSSOM이라는 트리 구조를 만들고 각 요소가 어떤 스타일을 포함하고 있는지에 대한 정보를 포함한다. 렌더 트리 : DOM, CSSOM의 결합으로 생성되고 화면에 표시되는 각 요소의 레이아웃을 계산하는데 사용

레이 아웃 : 렌더 트리가 완성되면 레이아웃 단계로 넘어가는데 화면 구성 요소의 위치나 크기를 계산하고, 해당 위치에 요소를 배치하는 작업을 한다.

페인트 : 화면에 배치된 요소에 색을 채워 넣는 작업을 한다. 효율적인 작업을 위해 구성 요소를 여러 개의 레이어로 나눠서 작업한다.

컴포지트 : 각 레이어를 합성하는 작업을 한다.

리플로우, 리페인트

리플로우, 리페인트

자바스크립트로 인해서 화면 내 어떤 요소의 너비와 높이가 변경되었다면 ?

  • 브라우저는 CSSOM을 새로 만들고 새로운 렌더 트리를 만들고 레이아웃 단계에서 다시 요소의 크기와 위치를 고려한다. -> 브라우저 렌더링 경로의 모든 단계를 모두 재실행하기 때문에 브라우저 리소스를 많이 사용한다.

  • 리페인트도 마찬가지로 브라우저 리소스를 많이 사용한다. 다만 리플로우와 차이점은 레이아웃 단계를 건너뛴다는 것

리플로우와 리페인트를 피하는 방법

  • transform

  • opacity

    이런 속성을 사용하면 해당 요소를 별도의 레이어로 분리하고 작업을 GPU에 위임하여 처리해서 레이아웃 단계와 페인트 단계를 건너뛸 수 있다. 이것을 하드웨어 가속이라고 한다. 요소를 별도의 레이어로 분리하여 GPU로 보내야 하는데, transform, opacity 속성이 이 역할을 한다.

  • 코드 찾기(Bargraph)

ScaleX안에 있는 width는 퍼센트 값, 1이하의 실수 값으로 바꿔주고 width가 0일때 막대의 너비가 0으로 줄어들 테고, width가 100이 되면 scaleX(1)이 되므로 width가 100%인 상태로 유지 될것이다.

컴포넌트 사전 로딩하기

사용자가 필요한 시점 바로 그 직전에 코드를 준비하는 것.

사전 로딩하는 시점

  • 사용자의 마우스가 버튼에 들어왔을 때

    마우스가 버튼에 들어오고 클릭하기 까지 300~600 밀리초의 시간차가 있다. 그사이에 새로운 파일을 로드하는 것

  • 컴포넌트의 크기가 커서 1초 이상의 시간이 필요하다면?

    useEffect에 넣어서 import할수 있다.

이미지 사전 로딩하기.

  • 이미지의 사이즈가 커서 제때 뜨지 않는다면??

    이미지도 컴포넌트의 import처럼 자바스크립트로 이미지를 직접 로드하는 방법이 있다.(보통은 html,css에서 이미지를 사용하는 시점에 로드된다.)

  1. Image 사용
const img = new Image();
img.src = 주소;
  1. useEffect
useEffect(() => {
  const img = new Image();
  img.src = "주소";
}, []);

intersection observer api 사용하기

적절한 이미지 포맷 사용하기

PNG

무손실 압축 방식, 투명도를 의미하는 알파 채널을 지원하는 이미지 포맷

JPG

손실 압축, 그만큼 이미지를 더 작은 사이즈로 줄일 수 있다.

webPack

무손실, 손실 압축을 제공하는 최신 이미지 포맷, 효율이 매우 좋음

  • Squoosh를 사용하여 이미지 변환하기. https://squoosh.app/ (opens in a new tab)

  • webp를 사용할 껀데 webp를 지원하지 않는 브라우저에서 문제가 될 수 있다.

  • picture 태그 사용

  • source는 srcset 속성을 사용한다.

      <picture>
        <source
          data-srcset={props.webp}
          type="image/webp"
          ref={imgRef}
        ></source>
        <img data-src={props.image} ref={imgRef} />
      </picture>