[FE Road Map] CSS 최적화

장동현·2022년 5월 23일
1

FE Roadmap

목록 보기
2/3
post-thumbnail

CSS 최적화

브라우저 로딩 과정

1. 파싱

  • 웹페이지 로드시 HTML 파일을 먼저 다운
    • 그 이후 HTML을 해석하여 DOM 트리를 생성
    • <script/> , <link/> <img/> 태그를 만나면 리소스 요청 후 다운
    • 이 과정에서 CSSOM 트리도 생성됨
  • DOM 트리 구성 HTML을 해석해 DOM을 생성하고
    각 DOM 객체를 트리 데이터 구조로 연결함HTML 태그는 항상 node와 맞물려짐
  • CSSOM 트리 구성 CSSOM → CSS Object Model
    • link를 통한 외부 스타일이나 내부 스타일 시트가 포함된 경우

    • CSS를 해석해서 CSSOM 트리를 생성함

2. 스타일

  • DOM 트리 + CSSOM 트리 → 스타일 매칭시켜주는 과정을 거쳐서 렌더트리를 구성하게 됨

3. 레이아웃

  • 노드의 정확한 위치와 크기를 계산
    • 루트노드부터 순회하여 계산하고
    • 결과로 각 노드의 정확한 위치와 크기를 픽셀값으로 렌더트리에 반영
    • IF → CSS 크기 값 % 레이아웃 단계를 거친 후 → 계산 → 픽셀 단위로 변환

4. 페인트

  • 이전 레이아웃 단계에서 계산된 값을 이용해서
  • 각 노드를 화면상의 실제 픽셀로 변환함
  • 위치와 관계없는 CSS 속성(색상, 투명도)을 적용함

변환된 픽셀 값은 어떻게 관리 되나요?

포토샵의 레이어처럼 생성되어 개별 레이어로 관리됨

- 모든 엘리먼트가 레이어가 되는 것은 아님
- transform 속성을 사용하면 엘리먼트가 레이어화 됨 → 이를 페인트

5. 합성과 렌더

  • 페인트 단계에서 생성된 레이어를 합성하여 스크린을 업데이트 함
  • 합성과 렌더 단계가 끝나면 화면에서 페이지 확인 가능함

이미지 출처 - hacks.mozilla.org

레이아웃과 리페인트 그리고 composition

  • 브라우저는 아래와 같은 순서로 렌더링을 진행함
    렌더링 과정은 상황에 따라 반복될 수 있음
  • layout → Paint → Composite 순서로 성능이 좋음
    Layout 변경 시키는 요소가 많을 수록 성능이 좋지 않다고 보면 됨

animation, transition → layout, paint, composition 중 어떤 것이 일어날지 알려줌

  • 좋음

  • 좋음

  • 안좋음 layout, paint, composition 다 변경됨

성능 개선 지표

그럼 어떤 기준으로 로딩 속도가 빠르고 느린지 판별할까?

브라우저 기준과 사용자 기준으로 나눌 수 있음

브라우저 기준의 성능 측정

  • 전통적인 성능 측정 방식은
    • 브라우저에서 발생하는 이벤트를 이용
    • DOMContentLoaded, load 이벤트 이용
  • DOMContentLoaded 이벤트
    • HTML과 CSS 파싱이 끝나는 시점
    • 렌더 트리를 구성할 준비가 된(DOM 및 CSSOM 구성이 끝난) 상황
  • load 이벤트
    • HTML 상에 필요한 모든 리소스가 로드된 시점

내비게이션 타이밍 API 이용

  • 브라우저에서 웹 페이지 성능 측정 하기 위해 제공

로딩 최적화

블록 리소스(CSS) 최적화

  • 렌더 트리 구성에서는 DOM 트리와 CSSOM 트리가 필요함

    • DOM 트리 → 파싱 중에 태그 발견 마다 구성가능하지만
    • CSSOM 트리 → 모두 해석해야 구성이 가능함
  • 우리가 head 태그 밑 배치하는 이유

<head>
  <link href="style.css" rel="stylesheet" />
</head>

1. 미디어 쿼리를 이용한 최적화

  • 특정 조건에서만 필요한 CSS의 경우
  • 미디어 쿼리를 사용하여 불필요한 블로킹을 방치 가능함
<link href="style.css" rel="stylesheet" />
<link href="print.css" rel="stylesheet" media="print" />
<link href="portrait.css" rel="stylesheet" media="orientation:portrait" />

2. @import 사용은 피하기

  • @import 사용 → 병렬적으로 다운이 불가하므로 → 로드 시간이 늘어남
/* foo.css */
@import url("bar.css")

3. 내부 스타일 시트를 사용하기

<head>
  <style type="text/css">
    .wrapper {
      background-color: red;   
    }
  </style>
</head>

4. 리소스 요청 수 줄이기

  • 이미지 스프라이트 기법
    • 아이콘 리소스 요청을 가정할 때
    • 우리는 아이콘 마다 요청해야 하면 이는 비효율적
    • → 한번에 모든것을 불러오자
  • 번들하기
    • webpack 같은 번들러를 사용하여 파일 요청을 줄이기

      <html>
        <head>
          <link href="bundle.css" rel="stylesheet" />
        </head>
        <body>
          <div class="foo">...</div>
          <script async src="bundle.js" type="text/javascript"></script>
        </body>
      </html>

5. 내부 스타일시트 사용하기

  • link 대신 style 태그를 사용
    • 단 내부 스타일 시트를 사용하면
    • 리소스 캐시를 사용 불가
    • HTML에 CSS가 매번 포함되므로 필요한 경우엔만 사용
<html>
  <head>
    <style type="text/css">
      .foo {
        background-color: red;
      }
    </style>
  </head>
  <body>
    <div class="foo">...</div>
  </body>
</html>

6. 작은 이미지를 HTML, CSS로 대체

  • 웹페이지에서 사용하는 아이콘 이미지 개수가 적은 경우
    • 이미지를 사용하는 대신 이미지를 HTML, CSS에 포함해 사용
  • 요청횟수를 줄일 수 있는 장점
  • 하지만 캐시 불가 하므로 필요한 경우만 사용
// befroe
.btn{background: url('../img/arrow_top.png') no-repeat 0 0;}
<img src="../img/arrow_top.png" />

// after
.btn{background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAOCAYAAAAbvf3sAAAAAXNSR0IArs4c6QAAAHBJREFUKBVjYBimICwsLAaEsXmPGV0QqnAeUNxfW1v7/tWrVy8hq0HRgKQ4CahoIxDPQ9cE14CseNWqVUtAJoMUo2tiBFkXGRmp9/fv3zNAZhJIMUgMBmAGMTMzmyxfvhzhPJAmmCJ0Gp8cutqhwAcASWgwk+79LiQAAAAASUVORK5CYII=') no-repeat 0 0;}
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAOCAYAAAAbvf3sAAAAAXNSR0IArs4c6QAAAHBJREFUKBVjYBimICwsLAaEsXmPGV0QqnAeUNxfW1v7/tWrVy8hq0HRgKQ4CahoIxDPQ9cE14CseNWqVUtAJoMUo2tiBFkXGRmp9/fv3zNAZhJIMUgMBmAGMTMzmyxfvhzhPJAmmCJ0Gp8cutqhwAcASWgwk+79LiQAAAAASUVORK5CYII=" />

7. 리소스 용량 줄이기

  • 요량이 큰 리소스 → 로딩 시간 느리게 함
  1. 중복 코드 제거
  2. 만능 유틸 사용 주의
// before
// 모든 것을 다 가져와서 용량이 큼
import _ from 'lodash';

_.array(...);
_.object(...);

// after
// 필요한 것만 가져오기
import array from 'lodash/array';
import object from 'lodash/fp/object';

array(...);
object(...);

8. HTML 마크업 최적화

  • 태그의 중첩을 최소화 하여 단순하게 구성
  • 공백 주석 등을 제거하여 사용
  • DOM 트리의 노드수 → 전체 1500개 미만
    • 최대 깊이 : 32개
    • 자식 노드를 가지는 부모 노드는 60개 미만
<html>
  <head>
    ...
  </head>
  <body>
    <p>hello</p>
  </body>
</html>

간결한 CSS 선택자 사용

  • id 대신 클래스 선택자를 사용하면 중복되는 스타일 묶어서 처리 가능
// before
<head>
    <style type="text/css">
      #wrapper {
        border: 1px solid blue; 
      }
      
      #wrapper #foo {
        color: red;
        font-size: 15px;
      }
      
      #wrapper #bar {
        color: red;
        font-size: 15px;
        font-weight: bold;
      }
      
      #wrapper #bar > span {
         color: blue;
         font-weight: normal;
      }
    </style>
</head>
<span id="foo">hello</span>
      <span id="bar">
        javascript <span>world</span>
</span>
// after
<head>
    <style type="text/css">
      .wrapper {
        border: 1px solid blue; 
      }
      
      .text {
        color: red;
        font-size: 15px;
      }
      
      .strong {
        font-weight: bold;
      }
      
      .wrapper .text {
        color: blue;
        font-weight: normal;
      }
    </style>
</head>
<span class="text">hello</span>
      <span class="text strong">
        javascript <span class="text">world</span>
</span>

레이아웃 최적화

  • 목표
    • JS 실행 과정과 렌더링이 다시 일어나는 과정에서
    • 레이아웃에 걸리는 시간을 단축하고
    • 레이아웃이 최대한 발생하지 않도록 하는 것임

1. 강제 동기 레이아웃 최적화

  • DOM 속성을 변경하면 화면 업데이트를 위한 레이아웃 발생 가능

    • 원래 레이아웃은 비동기이나 → 특정 상황에서 동기적으로 발생 가능
    • 특정 속성을 읽을 때 최신 값을 계산하기 위해 레이아웃 동기 발생
    • 이를 강제 동기 레이아웃
    • 실행 시간을 늘리기 때문에 신경써야 함
  • 강제 동기 레이아웃 피하기 - offsetHeigth, offsetTop

const tabBtn = document.getElementById('tab_btn');

tabBtn.style.fontSize = '24px';
console.log(testBlock.offsetTop); // offsetTop 호출 직전 브라우저 내부에서는 동기 레이아웃이 발생한다.
tabBtn.style.margin = '10px';
// 레이아웃

2. 레이아웃 스래싱(thrashing) 피하기

  • 한 프레임 내에서 강제 동기 레리아웃이 연속적으로 발생하면
    • 성능이 저하됨
function resizeAllParagraphs() {
  const box = document.getElementById('box');
  const paragraphs = document.querySelectorAll('.paragraph');

  for (let i = 0; i < paragraphs.length; i += 1) {
    paragraphs[i].style.width = box.offsetWidth + 'px';
  }
}
// 레이아웃 스래싱을 개선한 코드
function resizeAllParagraphs() {
  const box = document.getElementById('box');
  const paragraphs = document.querySelectorAll('.paragraph');
  const width = box.offsetWidth;

  for (let i = 0; i < paragraphs.length; i += 1) {
    paragraphs[i].style.width = width + 'px';
  }
}

What forces layout/reflow. The comprehensive list.

3. 가능한 한 하위 노드의 DOM을 조작하고 스타일을 변경하기

  • DOM을 변경하면
    1. 스타일 계산
    2. 레이아웃
    3. 페인트
    • 과정이 필요하므로 상위에 있으면 한 프레임 계산 시간 길어짐

체크 항목

  • DOM 트리 상위 노드의 스타일 변경 → 하위에 영향

  • 변경 범위를 최소화 → 레이아웃 범위가 줄어듬

  • 영향 받는 엘리먼트 제한하기

체크 항목

  • 부모-자식 관계 : 부모 엘리먼트의 높이가 가변적인 상태에서 자식 엘리먼트의 높이를 변경할 경우, 부모 엘리먼트부터 레이아웃이 다시 일어난다. 이때 부모 엘리먼트의 높이를 고정하여 사용하면 하단에 있는 엘리먼트는 영향을 받지 않게 된다. 예를 들어 높이가 모두 다른 여러 개의 탭 콘텐츠가 있을 때, 부모 엘리먼트(탭 컨테이너)의 높이를 고정하여 사용한다.
  • 같은 위치에 있는 엘리먼트 : 여러 개의 엘리먼트가 인라인(inline)으로 놓여 있을 때 첫 번째 엘리먼트의 width 값 변경으로 인해 나머지 엘리먼트의 위치 변경이 일어나므로 유의한다.

4. 숨겨진 엘리먼트 수정

  • disaplay : none → 레이아웃 리페인트 발생 X
    • 많은 수의 엘리먼트 변경시
    1. 숨겨진 상태에서 엘리먼트를 변경하고
    2. 다시 보이도록 하여 레이아웃 발생을 최소화

5. CSS 규칙수 최적화

  • 엘리먼트의 클래스를 변경하면 렌더링 발생
    • CSS 복잡하고 많을 수록 스타일 계산과 레이아웃 오래걸림

6. DOM 깊이 최적화

  • DOM이 작고 깊이가 얕을수록 계산이 빠름
  • 불필요한 엘리먼트 제거

7. 애니메이션 최적화

  • 한 프레임 처리 16ms(60ms) 내로 완료되어야 렌더링 시 끊김 현상 X
  • JS 실행 시간이 10ms 미만 → 레이아웃, 페인트 과정 포함해도 16ms 미만으로 구현 가능
    • 고로 CSS 사용 애니메이션 구현 권장의 이유

8. requestAnimationFrame() 사용

브라우저의 프레임 속도에 맞춰서 애니메이션을 실행시킬 수 있도록 도와줌

  • setInterval, setTimeout과 다르게 프레임을 시작할 때 호출
  • 일정한 간격으로 애니메이션 수행 가능

페이지가 보이지 않으면 콜백함수가 호출되지 않음

9. CSS 애니메이션 사용

CSS3 애니메이션을 사용하면 JS 실행할 필요가 없고

브라우저가 애니메이션 처리 최적화 → 부드러운 애니메이션 사용 가능

  • poistion : absolute

    • 애니메이션 영역이 주변 영역에 미치지 않도록
    • absolute, fixed 사용
  • transform 사용

    • position, width, heigth → 레이아웃 변경 발생
    • transform 사용 엘리먼트 → 레이어로 분리
    • 레이아웃, 페인트 줄일 수 잇음
    • 합성만 발생 → 렌더링 속도 향상(composition)
    • 하드웨어 지원시 GPU 사용 가능
/* bad */
@keyframes move {
  50% {
    top: 100px;
    left: 100px;
  }
}

/* good */
@keyframes move {
  50% {
    transform: translate(100px, 100px);
  }
}

참고

profile
FE 개발자 장동현 입니다 😃

0개의 댓글