FE 성능 최적화

패스트캠퍼스 the red를 듣고 정리해보았다.

파일 다운로드

최신 브라우저는 대체로 도메인 당 6개의 접속만 동시에 처리한다(HTTP 프로토콜 /1.1기준)
http2에서는 하나의 연결로 계속 파일을 주고받아서 괜찮다.
하지만 아직은 1.1기준으로 작동하는 사이트가 많다.

한 도메인에서 6개의 파일만 동시에 다운로드 할 수 있다.
6개 이상부터는 큐에 넣어두고 하나씩 다운로드 하게 된다.

이미지와 같은 파일들은 사용자가 기다리는것에 익숙하지만
js나 cs파일들의 다운로드가 느려지면 페이지에 변화가 없고 인터액션이 늦어지게 된다.

개선방안

  1. 파일 갯수와 용량을 작게한다.

  2. 별도 도메인이나 CDN등으로 분산하여 해결할 수 있다.

  3. 적절한 포맷을 사용한다. 이미지의 경우 webp파일을 사용하여 크기를 줄일 수 있다.
    ex) 레진코믹스

1
2
3
4
5
6
<!-- 지원 타입에 따라 다른 이미지 로딩 -->
<!-- img 태그를 picture태그로 감싸서 picture태그를 지원하는 브라우저에서는 picture를 해석하도록 만들었다. -->
<picture>
<source srcset="img/photo.webp" type="image/webp" />
<img src="img/photo.jpg" alt="my photo" />
</picture>
  1. 화면 크기와 해상도에 따라 적절한 이미지 로딩
1
2
3
4
5
6
7
8
<!-- 화면 크기에 따라 적절한 이미지 로딩 -->
<picture>
<source srcset="img/photo_small.jpg" media="(max-width:800px)" />
<img src="img/photo.jpg" alt="my photo" />
</picture>

<!-- 해상도에 따라 적절한 이미지 로딩 -->
<img src="img/icon72.png" alt="icon" srcset="img/icon14.png 2x" />
  1. 웹 폰트 최적화
  • 구글폰트: 나눔 고딕 등 한국어 폰트 26종 제공
  • WOFF2는 WOFF,TTF등에 비해 30% 용량이 작다.
  • 필요한 글자만 골라서 글꼴을 만들 수도 있다.(메뉴용 글꼴 등)
  1. 화면 크기 등에 따라 필요한 스타일 시트만 로딩
  • 하지만 실제 이렇게 사용하는 경우는 없다. css는 크기가 작고 파일 개수를 늘리는 것은 좋지 않다.
  • 부트스트랩이 차지하는 용량이 20kb쯤
1
2
3
4
5
6
<link href="mobile.css" rel="stylesheet" media="all" />
<link
href="desktop.css"
rel="stylesheet"
media="screen and (min-width: 600px)"
/>
  1. link 태그 만으로 접속 시간 절약
1
2
3
<link rel="dns-prefetch" media="https://taegon.kim" />
<link rel="preconnect" media="https://cdn.example.com" />
<link rel="prefetch" media="https://cdn.example.com/library.min.js" />

Preconnect를 사용할 경우 외부 도메인을 미리 검색하여 소켓을 연결해둔다. 서드파티 자원 연결에 적합하다.

Prefetch를 사용할 경우 외부 도메인을 미리 검색하여 소켓을 연결 후 다운로드까지 해둔다.
하지만 남용하면 안되고 꼭 필요하거나 다음 경로가 명확하게 예상이 될 경우에만 사용해야 한다.

로딩 속도 개선

  1. 필수 컨텐츠가 아니라면 비동기 로딩을 고려해보자(광고,댓글,헤더/푸터 등)

  2. 이미지/아이프레임/스크립트 등은 필요할 때까지는 읽지 않는 게으른 로딩(lazy loading)기법을 고려해보자. 코드를 분할한 후 필요할 때 읽어오는 방식이다. 웹팩에서 자동으로 해주기도 한다.

1
2
<img src="image.jpg" loading="lazy" />
<iframe src="https://example.com" loading="lazy"></iframe>
  1. 시간이 많이 걸린다면 플레이스 홀더 등으로 대체한다.

-스켈레톤 ui

계산 시간

  1. 웹 워커

자바스크립트는 싱글스레드이다. 하지만 워커스레드를 이용하여 멀티스레드가 가능하다.

워커스레드

  • UI를 조작할 수 없다.
  • 워커 스레드 전용으로 분리된 파일이 필요하다.
  • postMessage()로 데이터를 전송하고, onMessage 이벤트를 통해 받는 방식으로 통신한다.
  1. 느긋한 계산

값이 필요해질 때 까지 계산을 미뤄두는 기법이다.

게으른 평가, 지연 평가라고도 한다.

  1. 메모이제이션

계산 결과를 기억해두고 반복 사용하는 기법.
루프, 재귀 호출 등 최적화.

반응시간

지나치게 느린 애니메이션 및 응답 속도는 사용자 경험을 저해한다.

  1. 시스템의응답이 40ms보다 느리면 주의력 분산이 발생함
  2. ui가 100ms 이하로 반응해야 ui를 다루고 있다고 느낀다.
  3. 애니메이션은 60fps를 기준으로 한다.

애니메이션의 렌더링 순서

js-> 스타일-> 레이아웃 ->페인트 -> 합성

페인트까지는 메인스레드에서 진행한다. 합성만 컴포지터 스레드에서 진행한다.

메인스레드는 자바스크립트가 동작하기 때문에 메인스레드의 작업이 많아지면 자바스크립트의 속도도 느려진다.

개선 방법

메인스레드에서 이루어지는 과정은 가능한 한 피하고 컴포지터 스레드에서 일어나도록 하면 된다.

즉 레이아웃이나 페인트는 피하고 합성단계를 유발하는 css 속성들을 주로 사용한다.

각 단계의 동작을 유발하는 css 속성은 https://csstriggers.com/에서 확인할 수 있다.

정리

  1. 가능하면 js보다는 css 애니메이션을 최대한 활용한다.
  2. 다음 css 속성 위주로 애니메이션 -gpu 가속을 적용한다.(자동으로 적용된다.)
  • transform:translate or scale or rotate
  • opacity
  1. 레이아웃 변경이나 리페인팅을 유발하는 css 속성은 비용이 많이 든다.
  • 레이아웃 : width,height,padding, margin,display
  • 페인트 : color,background,outline,box-shadow
  1. setTimeOut 보다는 requestAnimationFrame, Web Animations API 등 활용
  • requestAnimationFrame: 브라우저가 최적화, 비활성 탭에서는 동작 안 함
  • web Animation API: css애니메이션과 같은 애니메이션 엔진 사용
  1. three.js velocity.js와 같은 고성능 애니메이션 라이브러리 사용

  2. will-change 속성을 통해 브라우저가 최적화 할 속성 명시

  • 남용하면 리소스 낭비,
  1. DOM 접근과 업데이트는 가능한 적게, 한 번에 몰아서 처리,
  • 엘리먼트 추가는 DocumentFragment 활용
1
2
3
4
5
6
7
8
9
10
const app = document.getElementById("app");
const frag = document.createDocumentFragment();

for (let i = 0; i < 1001; i++) {
const el = document.createElement("div");
el.innerText = `Element ${i}`;
frag.appendChild(el);
}

app.appendChild(frag);

리소스

cpu 점유율, 전력 소비량, 스토리지, 메모리 사용량, 네트워크 트래픽 중 메모리 누수와 네트워크 트래픽을 신경써야 한다.

메모리 누수

프로그램이 필요하지 않는 메모리를 계속 점유하는 현상

메모리 생명 주기

  1. 할당 : 사용할 메모리를 확보한다.
  2. 사용 : 메모리를 사용한다.
  3. 헤재 : 불필요한 메모리를 반환한다.

개선 방법

js는 가비지 컬렉션을 통해 메모리를 해제한다.

이 메모리를 몇개의 변수가 참조하는지 카운트하여 0개일 경우 지운다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var x = {
a: {
b: 2,
},
};
// 두 객체 모두 생성된다.
// x 객체를 o1 a객체를 o2라고 하자

var y = x; //y도 o1를 참조한다.

x = 1; // x는 이제 o1를 참조하지 않는다. 이제 o1를 참조하는 변수는 y가 유일하다.

var z = y.a; // o1 내부의 o2을 z가 참조한다.

y = "mozilla"; // y는 o1를 참조하지 않지만 z변수가 o1의 o2를 참조하므로 o1은 제거되지 않는다.

z = null; // 이제 o1과 o2는 아무 곳에서도 참조가 되지않아 가비지 컬렉션이 된다.

순환참조 문제

객체끼리 참조가 맞물려서 가비지 컬렉션이 동작하지 않는 문제

마크스위프 알고리즘을 통해 해결할 수 있게 되었다.

메모리 누수의 대표적 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// case 1 : 큰 전역 변수는 계속 메모리에 존재한다.
const bigData={.../*big data*/};

// case 2 : 클로저는 생성된 실행 컨텍스트의 변수를 해제하지 못하게 한다.
function factory(){
const largeData={.../*big data*/}

return ()=>{
//...
}
}

const fn=factory();
fn();
// fn을 실행한 후에도 여전히 largeData는 메모리에 존재한다.

// case 3: 큰 데이터가 다른 변수에서 참조되면 큰 데이터는 해제되지 않는다.
const data1={linkTo:bigData};
const data2={anotherName:bigData};

개선방안

아래의 코드는 예시일 뿐이다.

실제로는 모듈이나 다른 함수로 분리해서 사용하는 것이 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// case 1 : 큰 전역 변수는 계속 메모리에 존재한다.
const bigData={.../*big data*/};

// case 2 : 클로저는 생성된 실행 컨텍스트의 변수를 해제하지 못하게 한다.
function factory(){
const largeData={.../*big data*/}

return ()=>{
//...
}
};

{
const fn=factory();
fn();
}
// fn은 실행 후 현재의 Lexical Enviroment와 함께 해제되고 largeData도 함께 해제된다.

// case 3: 큰 데이터가 다른 변수에서 참조되면 큰 데이터는 해제되지 않는다.
const data1={linkTo:bigData};
const data2={anotherName:bigData};

네트워크 트래픽

파일 용량을 줄이거나 필요할 때만 불러와서 트래픽 낭비를 줄인다.

  1. 최소화된 js,css파일을 사용한다.
  2. 프레임워크는 한 개 이하만 사용한다.
  3. 파일 주소의 파라미터는 주의해서 사용, 의도하지 않은 캐시버스터가 될 수 있다.
  • 브라우저는 전체 url 기반으로 캐시하기 때문에 업데이트가 되어도 받아오지 못할 수 있다.
  • 일부러 랜덤한 숫자를 붙여 새로 캐시되도록 하는 것이 캐시버스터이다.
  1. 이미지, 미디어는 필요할 때 불러오는 게으른 로딩(lazy loading)기법을 사용한다.

React Image preload!

프론트에서 최적화를 하기 위해 이미지 자체를 webp등으로 최적화하는 방법이 있지만 다른 방법으로는 미리 로딩하고 캐시로 가져오는 preload 방식이 있다.

Preload

이미지를 미리 로딩하고 캐시로 가져오는 방식이다.
캐시는 브라우저차원에서 자동으로 하기 떄문에 이미지를 미리 받아오기만 하면 된다.

Preload 에는 두 가지 방식이 있다.

병렬(Parallel) 방식

자바스크립트의 경우 요청을 병렬로 처리하려고 하므로 for문을 돌면서 여러번 호출해주면 된다.

리액트의 경우 useEffect에서 첫 렌더링시에 호출하여 이미지를 미리 로드해두고 캐시해서 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
useEffect(() => {
function preloading(imageArray) {
imageArray.forEach((url) => {
const image = new Image();
image.src = url;
});
}

preloading(["1.png", "2.png", "3.png"]);
}, []);

특징

모든 요청을 한 번에 보내기 때문에 전체 사진을 불러오는데에 시간이 줄어든다. 하지만 전체 사진이 로드되기 전까지 기다려야한다.

modules

순차적(Sequential) 방식

순차적 방식은 재귀를 통해 구현한다. 이미지의 onload 이벤트에 다음 이미지를 parameter로 재귀호출하도록 하여 이미지가 우선순위를 갖고 캐싱될 수 있도록 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
useEffect(() => {
function preload(imageArray, index) {
index = index || 0;
if (imageArray && imageArray.length > index) {
const image = new Image();
image.onload = function () {
preload(imageArray, index + 1);
};
image.src = images[index];
}
}

preload(images);
}, []);

특징

병렬 방식에 비해 전체 작업시간은 늘어나지만 이미지들이 하나씩 로드되어 하나씩 확인할 수 있다.

modules

마무리

  • 병렬 : 빠르게 불러와야 할 때

  • 순차적 : 조금씩이라도 불러와야할 때, 추가로 슬라이더 등을 구현할 때 다음 이미지를 이 방식으로 미리 불러오면 좋을 것 같다.

ref

image preloading
이미지 프리로딩

lazy initial state

리액트 공식문서에 정리된 내용이다.

lazy initial state

보통 useState는 다음과 같이 사용한다.

1
const [state, setState] = useState(initialState);

리액트에게 initialState가 필요한 순간은 첫 렌더링시 뿐이다.

하지만 함수형 컴포넌트의 경우 리렌더링마다 함수 본문을 실행한다.
이는 값이 필요하지 않더라도 계속해서 계산한다는 이야기이다.

아래 코드와 같이 initialState 계산비용이 비싼 경우 성능에 영향을 끼칠 수 있다.

1
const [state, setState] = useState(someExpensiveComputation(props));

이런 불필요한 계산을 방지하기 위해 리액트에선 lazy initial state라는 기능을 제공한다.
useState 안에 콜백함수를 제공할 시 초기 렌더링 시에만 사용하고 그 이후의 렌더링에는 무시된다.

다음과 같은 코드 작성시 첫 렌더링 시에만 계산한다.

1
const [state, setState] = useState(() => someExpensiveComputation(props));

정리

  1. initial state의 계산비용이 클 경우에 useState에 콜백함수를 전달하여 첫 렌더링 시에만 값을 계산할 수 있다.

  2. 단순 원시값(number,string,etc..)의 경우 계산이 필요하지 않기 때문에 오히려 성능 저하로 연결될 수 있다.

크롬 개발자 도구 perfomance탭 알아보기

크롬의 개발자도구의 성능분석 탭을 사용해보자.
다음 문서의 예시를 따라 진행하였다.

동작원리

성능 분석은 웹페이지를 녹화하고 분석하는 방식으로 진행된다.

시작하기

성능(Perfomance)탭은 크롬 개발자도구에 있다.

다음과 같은 순서로 시작할 수 있다.

  1. 개발자 도구를 열고 perfomance탭에 들어간다.

성능 탭

  1. 예제를 신행하고 싶을 경우 다음 링크를 시크릿 모드에서 연다. 크롬 익스텐션이 성능 측정에 노이즈를 발생시킬 수 있기 때문이다.

  2. 시작한다.

    • 초기 랜더링부터 분석 하고싶은 경우 새로고침 버튼을 누르면 된다.
    • 혹은 성능을 분석하고 싶은 시점 전에 녹화버튼을 누르면 된다.
    • stop버튼을 눌러 녹화를 중지할 수 있다.
    • 너무 길게 녹화할 필요 없이 3~4초 정도 녹화하면 된다.

모바일 CPU 시뮬레이션

모바일 기기는 데스크탑이나 노트북에 비해 CPU의 성능이 좋지않다.
CPU의 성능을 일부러 저하시켜 모바일 환경에서의 시뮬레이션을 할 수 있다.

  1. 성능 탭에서 스크린샷(screenshot)이 활성되어있는지 확인한다.
  2. 캡쳐 설정(톱니바퀴)를 클릭한다.
  3. 아래의 CPU제한없음에서 CPU에 제한을 준다.
    예시에선 2배 제한을 줬지만 예전 아티클인지 4배 8배만 남아있어서 4배 제한을 주었다.

모바일 시뮬레이션

결과 분석

위의 시작하기에서 얻은 결과를 분석해보자.

분석 결과

초당 프레임 분석

애니메이션의 성능을 측정하는 주요 지표는 초당 프레임수(FPS)이다.
사용자들은 애니메이션이 60 FPS로 실행될 때 행복하다.

  1. FPS 차트
    빨간색 막대를 볼 때마다 Framerate가 저하된다는 의미이다.
    녹색 막대가 높을수록 FPS가 높다.

    FPS 차트

  2. CPU 차트
    CPU 차트의 색상은 그림에서 아래의 요약탭의 색상과 일치한다.
    색칠이 되어있을 수록 CPU를 사용했다는 의미이다.
    장시간 색칠되어있을 경우 최적화가 필요하다는 의미이다.
    CPU 차트

FPS,CPU 차트 위에 마우스를 올리면 해당 시점의 스크린샷을 보여준다.

  1. 프레임
    프레임
    마우스를 올리면 특정 프레임에 대한 FPS를 보여준다.
    위 사진과 Frame 영역의 색이 다른 건 사각형 수를 조절했기 때문이다.

    • 실시간 FPS 추정치
      ctrl+shift+p를 누르고 FPS 추정치를 확인할 수 있다.
      실시간 FPS 추정치

    다음과 같이 실시간으로 FPS 추정치를 보여준다.

병목 현상 찾기

  1. 요약 탭

    • 선택한 이벤트가 없는 경우 활동 내역을 보여준다.
    1. 사각형 수가 없는 경우
      유휴상태가 많고 작업이 여유로움을 알 수 있다.

    사각형 수가 없는 경우

    1. 사각형 수가 많은 경우
      사각형 수가 늘어날 수록 랜더링에 대부분의 시간이 쓰이는 것을 알 수 있다.

    사각형 수가 많은 경우

    1. 사각형 수가 많은 경우+ 최적화
      여전히 랜더링에 대부분의 시간이 쓰이지만
      애니메이션을 담당하는 스크립트에서 작업시간을 줄였음을 알 수 있다.

    최적화를 해줄 경우

  2. 기본 탭
    특정 프레임을 클릭하고 기본탭을 열어 시간 경과에 따른 메인스레드의 활동도를 볼 수 있다.
    각 막대는 이벤트이며 막대의 길이가 길 수록 오래 걸렸다는 것을 의미한다.
    또한 막대가 쌓인 것은 아래의 이벤트가 위의 이벤트를 발생 시켰다는 의미이다.
    기본 탭

    또한 이벤트를 클릭하여 단일 이벤트에 대한 정보를 확인할 수도 있다.
    기본 탭 단일 이벤트

    호출 스택을 통해 이벤트를 발생시킨 소스코드 또한 확인할 수 있다.

    이벤트를 발생시킨 소스코드

hexo + icarus 블로그 lighthouse 성능지표 개선하기

블로그의 성능면에서 개선할 것이 없나 고민하던 중 크롬에서 제공하는 성능지표인 lightouse 분석을 하고 개선하기로 하였다.

lighthouse

내 블로그의 lighthouse 점수이다.
퍼포먼스쪽이 처참하다.
어차피 다 수정해야겠지만 빨간 권장사항들을 먼저 수정하기로 하였다.
lighthouse 점수

적절한 이미지 size 사용하기

첫 번째 권장사항이다. 적절한 크기의 이미지를 사용하라는 지침이다.
나는 블로그에 사진을 업로드 할 때 따로 이미지의 크기를 지정해주지 않아 리페인트 작업에 많은 리소스를 사용한 것 같다.
또한 png파일들을 주로 사용하다보니 이미지의 크기가 큰 것도 영향이 있었다.

Serve images that are appropriately-sized to save cellular data and improve load time.
lighthouse 이미지 개선

정적 이미지 사용하기

현재 나는 깃허브 issue를 이용해 이미지를 관리하고 있다.
또한 이미지 형식이 png라 많은 리소스를 사용한다.
이를 수정해주었다.

source에 postimage라는 폴더를 새로 만들어주었다.

폴더구조

정적파일을 생성할 떄에 source안의 폴더들은 public안에도 생성된다.

정적파일 폴더구조

이 작업들을 하나씩 다 해야한다..

이미지의 크기 명시하기

이미지는 파일 자체의 크기도 크고 이미지 사이즈를 명시하지 않으면 리페인트 작업에 리소스를 사용하게 된다.
이미지 태그를 사용하여 사이즈를 명시하고 이미지를 불러주었다.

수정 전

1
![lighthouse 개선](https://user-images.githubusercontent.com/79688915/144422469-6fc049ec-191b-4ca1-ae0e-d667723971d2.png)

수정 후

1
<img src="/postimage/hexo_icarus_블로그_성능지표_개선하기/성능.png" alt="lighthouse 개선" width="650" height="300px">

해결 방안

lighthouse 개선