728x90
반응형
728x90
반응형
반응형

웹 페이지의 성능 최적화는 로딩최적화와 렌더링최적화 두 단계로 나뉜다.

로딩 최적화

브라우저 렌더링

5단계

  • 파싱
    • html을 해석해 DOM 트리 구축
    • html에 포함되어 있거나 리소스로부터 다운 받은 css를 해석해 CSSOM 트리 구축
  • 스타일 계산
    • DOM에 CSSOM 정보 매칭 -> render 트리 그림
      • script, meta, link 태그는 렌더링에 반영하지 않음
      • CSS로 감춘 노드 역시 렌더 트리에 포함하지 않음
  • 레이아웃(=리플로우)
    • 브라우저의 뷰포트 안에서 노드가 가져야 할 정확한 위치와 크기를 계산
    • 객체의 정확한 크기와 위치를 파악하기 위해 루트부터 노드를 순회하면서 계산
    • 레이아웃 결과로 각 노드의 정확한 위치와 크기값을 픽셀값으로 렌더 트리에 반영
  • 페인트
    • 레이아웃에서 계산된 값을 이용해 렌더트리의 각 노드를 화면 상의 실제 픽셀로 변환함
    • 위치와 관계 없는 CSS 속성들이 적용됨 (색상, 투명도 등)
  • 합성 & 렌더
    • 페인트 된 레이어들을 합성하여 스크린을 업데이트
    • CSS Transform 동작

상세: https://hacks.mozilla.org/2017/08/inside-a-super-fast-css-engine-quantum-css-aka-stylo/

 

로딩 최적화

로딩 최적화 기준: 브라우저

  • W3C Navigation Timing API - Processing Model: https://www.w3.org/TR/navigation-timing-2/
  • 전통적인 로딩 최적화의 기준이 됨.
  • DOMContentLoaded, Load 이벤트 시점이 빠르다 => 브라우저가 페이지에 포함된 리소스를 준비하는 것이 빠르다.

로딩 최적화 기준: 사용자

  • 같은 크기의 리소스, 같은 타이밍에 Load이벤트가 발생해도 얼마든지 사용자 입장에서 더 빨라 보이는 페이지가 있다.
  • 조금조금씩 보여주면 사람들은 기다릴 수 있다(https://web.dev/articles/critical-rendering-path?hl=ko)

관련 지표들

  • First Contentful Paint (FCP)
  • Largest Contentful Paint (LCP)
  • Speed Index (SI)
  • Interaction to Next Paint (INP)
  • Toatal Blocking Time (TBT)
  • Cumulative Layout Shift (CLS)

로딩, 렌더링 지표 비유

  • 뭔가 진행되고 있구나 : FP, FCP
  • 이제 원하는 내용을 읽을 수 있다 : LCP
  • 이제 얼추 동작하는구나 : FID, INP, TTI
  • FCP, LCP, INP를 향상시키는데 주요한 리소스 기준으로 최적화
  • 메인 섹션의 컨텐츠, 현재 경로의 내용등을 먼저
  • 메뉴, 배너, 공지사항, 분석툴, 다음 경로 캐싱등을 나중에

 

CSS 최적화

CSS : render blocking resource

  • 렌더 트리를 구성하기 위해서는 DOM 트리와 CSSOM 트리가 모두 필요함
  • DOM 트리는 순차적으로 구성될 수 있지만, CSSOM 트리는 전체 CSS 를 모두 해석해야 구성 가능 (캐스캐이딩 방식)
  • CSSOM 트리가 구성되기 전까지는 렌더 트리를 만들 수 없음
  • media 속성에 따라 Blocking을 피할 수 있음

최적화 가이드

  • CSS 는 항상 최상단 (head 영역)에 배치한다.
<head>
  <link href="style.css" rel="stylesheet">
</head>
  • media 쿼리를 올바르게 사용한다.
<link href="style.css"    rel="stylesheet">
<link href="style.css"    rel="stylesheet" media="all">
<link href="portrait.css" rel="stylesheet" media="orientation:portrait">
<link href="print.css"    rel="stylesheet" media="print">
  • @import 를 사용하지 않는다.
  • 경우에 따라 CSS 를 HTML에 인라인으로 포함시킨다.(네트워크 요청 수 줄이기)

 

자바스크립트 최적화

자바스크립트 : parser blocking resource

  • 자바스크립트는 DOM 과 CSSOM 을 동적으로 변경할 수 있음
  • 자바스크립트는 자신이 실행되기 직전까지의 DOM 트리에만 접근이 가능함
  • HTML 파싱 과정에서 script 태그를 만나면 스크립트 실행이 완료될 때까지 DOM 트리 생성이 중단됨
  • 외부 자바스크립트의 경우 모든 스크립트가 다운로드 된 후 실행될 때까지 DOM 트리 생성이 중단됨
  • 자바스크립트 실행은 CSSOM 이 완성될 때까지 중단됨

최적화 가이드

  • 자바스크립트는 항상 문서의 최하단 (</body> 직전) 에 배치
<body>
  <div>...</div>
  <div>...</div>
  <script src="app.js" type="text/javascript" />
</body>
  • 초기 렌더링에 쓰이지 않는 스크립트는 defer, async 속성을 명시하여 Blocking 을 방지
    • defer IE10 >= 지원
    • async는 순서보장 없음
    • defer는 순서보장(IE9에서는 순서보장안됨)
    • async vs defer attributes
<head>
    <link rel="preload" href="style.css" as="style">
    <link rel="preload" href="main.js" as="script">

    <link rel="stylesheet" href="style.css">
</head>
<body>
    ...
    <script src="https://google.com/analatics.js" type="text/javascript" defer />
    <script src="main.js"></script>
</body>

 

리소스 최적화

  • 브라우저, 사용자기준에서 모두 효과적
  1. 요청(Request) 줄이기
  2. 실제 리소스의 용량 줄이기

 

요청을 줄이는 방법

  1. 중복되거나 불필요 파일 제거
  2. 하나의 JS, CSS 파일 사용
    • webpack을 이용한 번들링
    • 단순한 concat도 의미가 있음
  3. HTML, CSS로 대체 가능한 이미지 제거(작은 이미지인 경우 base64 이미지도 방법)
  4. 이미지 Sprites
    1. 작은 이미지가 여러개 들어간 웹앱
    2. css의 backgound-position을 설정해 사용
    3. Webpack 사용자의 경우 webpack-spritesmith를 사용하여 이미지 합치기와 css 설정 등 자동화 가능

 

불필요 데이터 제거

  • CSS
    • 간결한 css selector 사용
    • 불필요한 css rule 제거
  • JS
    • 만능 util.js 정리
    • 오버스펙 라이브러리 지양
    • 파일에 포함된 sourcemap 제거
  • HTML 마크업 최적화
    • HTML을 단순하게 구성한다 (태그의 중첩을 최소화한다)
    • 공백, 주석 등을 제거한다

Minify (Uglify)

  • HTML, Javascript, CSS 모두 Minify 해서 사용
  • 불필요한 주석이나 공백등을 제거할 수 있음

 

<요약>

네트워크 리소스 최소화

  • 네트워크 요청의 개수를 줄인다 (Javascript 와 CSS 파일을 가능한한 합친다)
  • 아이콘이 많은 경우 이미지 스프라이트를 사용한다
  • HTML, CSS, Javascript 를 Minified 해서 사용한다

크리티컬 렌더링 패스 최적화

  • HTML 과 CSS 의 구조를 최대한 단순하게 만든다
  • CSS는 HTML 문서의 최상단에 배치한다
  • CSS의 media 타입을 정확하게 지정한다
  • 크리티컬 CSS는 인라인 시킨다
  • Javascript 는 HTML 문서의 최하단에 배치한다
  • 초기 로딩과 렌더링에 꼭 필요없는 Javascript는 async나 defer 속성을 사용한다

렌더링 최적화

DOM 조작으로 인한 렌더트리 변경

JavaScript

  • 자바스크립트 코드를 통해 동적으로 Style 속성 or 클래스명을 변경
  • CSS Animation (Transition) 등으로 인한 변경도 이 과정에 속함

Style 계산

  • 어떤 CSS 룰이 어떤 DOM 요소에 적용되어야 하는지를 계산하는 과정

Layout

  • 실제 그려질 좌표정보를 픽셀단위로 계산하는 과정
  • 자식이나 형제 요소들에게 영향을 줌

Paint

  • Layout 과정에서 정해진 영역에 픽셀을 채우는 과정

Composite

  • 각각의 분리된 레이어들을 합성하는 과정

 

리플로우와 리페인트

  • 리플로우는 전체 픽셀을 다시 계산해야 하기 때문에 부하가 큼
  • 리페인트는 실제 계산된 픽셀로 화면에 그리는 과정이기 때문에 주로 부하가 적음

DOM 트리 변경

  • DOM 추가 / 삭제
  • 리플로우 발생

위치나 사이즈 변경

  • CSS 속성 중 높이, 넓이, 위치 등에 영향을 주는 속성값 변경
  • height, width, left, top, font-size, line-height 
  • 리플로우 발생

색상이나 투명도 설정

  • CSS 속성 중 높이, 넓이, 위치 등에 영향을 주지 않는 속성값 변경
  • background-color, color, visibility, text-decoration 
  • 리페인트 발생

레이아웃 최적화 방법

CSS 규칙 수 최소화

  • 사용하는 규칙이 적을 수록 계산이 빠름
  • 복잡한 selector도 지양해야 함

DOM 깊이 최소화

  • 문서가 작고 얕을 수록 계산이 빠름

가능한한 하위 노드의 스타일을 변경

  • DOM 트리 상위 노드의 스타일을 변경하면 하위 노드에 모두 영향을 미치기 때문
  • 변경범위를 최소화할수록 레이아웃 범위가 줄어듦

숨겨진(display: none) 엘리먼트 수정

  • 숨겨진 엘리먼트를 변경할 경우에는 레이아웃이 동작하지 않음

영향 받는 엘리먼트 제한

  • 변경으로 인해 영향을 받는 엘리먼트를 제한 해야 함
  • ex: 콘텐츠로 인해 높이가 변경 될 경우 위 아래 위치한 엘리먼트들에 위치에도 영향을 줌
    • fixed 혹은 absolute position을 사용하여 영향을 받은 엘리먼트를 제한 해야 함

애니메이션

목표 : 60fps

  • 한 프레임에 대한 처리가 16ms 내로 완료되어야 함 (브라우저가 동작하는 시간을 고려하면 10ms 이내)

requestAnimationFrame() 사용

  • 브라우저의 프레임 속도 (60fps) 에 맞추어 애니메이션을 실행할 수 있도록 해줌(혹은 모니터 주사율에 맞추어준다)
  • 정확한 시간에 호출됨(프레임 시작 시 호출), setTimeout, setInterval은 프레임 종료 시 호출되므로 일정하지 않음
  • 현재 페이지가 보이지 않는 상태인 경우에는 렌더링이 발생하지 않도록 해 줌

position: absolute 처리

  • position을 absolute나 fixed로 설정하면 주변 레이아웃에 영향을 주지 않음
  • 애니메이션 영역이 주변 영역에 영향을 주지 않도록 할 것

transform 사용

  • position, width, height 등은 Layout 을 발생시킴
  • transform 은 Composite 만 발생시키기 때문에 훨씬 빠름 (GPU 사용)

구형 브라우저의 일괄처리

동일한 요소의 스타일을 여러 번 변경

const myelement = document.getElementById('myelement');

myelement.style.width = '100px';
myelement.style.height = '200px';
myelement.style.margin = '10px';
  • 3번의 리플로우가 예상됨

스타일 변경은 한 번에 모아서 처리

<style>
  .newstyles {
      width: 100px;
      height: 200px;
      margin: 10px;
  }
</style>

<script>
  const myelement = document.getElementById('myelement');
  
  myelement.classList.add('newstyles');
</script>
  • 클래스 사용으로 한 번으로 줄일 수 있음

그 외

  • DOM 요소 추가 시에도 appendChild 를 여러 번 하면 리플로우 여러 번 발생 -> innerHTML 사용
  • 최신 브라우저에서는 최적화 필요 없음

강제 동기 레이아웃

강제 동기 레이아웃 피하기

  • 스타일 변경 후 offsetHeight, offsetTop과 같은 계산된 값을 요청할 경우 강제로 레이아웃을 수행함
const tabBtn = document.getElementById('tab_btn');
const menuBtn = document.getElementById('menu_btn');

tabBtn.style.fontSize = '24px';

console.log(testBlock.offsetTop); // offsetTop 호출 직전 레이아웃

tabBtn.style.margin = '10px';
// 레이아웃
  • 레이아웃 이유
    • 계산된 값을 반환하기 전에 변경된 스타일이 계산 결과에 적용되어 있지 않으면 변경 이전 값을 반환하기 때문
  • 최신 브라우저에도 영향 받는 부분이므로 강제 동기 레이아웃을 하지 않도록 주의해야 함

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

  • 많은 레이아웃을 연속적으로 빠르게 실행하는 경우 강제 동기 레이아웃에서 더 좋지 않은 결과를 나타냄
function resizeAllParagraphs(paragraphs) {
  for (let i = 0; i < paragraphs.length; i += 1) {
    paragraphs[i].style.width = box.offsetWidth + 'px';
  }
}
  • 이 코드는 단락 그룹을 반복 실행하고 각 단락의 너비를 상자의 너비와 일치하도록 설정함
  • 반복문 안에서 style.width를 설정하고 box.offsetWidth를 호출함으로 인해 매 반복 시 마다 레이아웃 발생
  • 레이아웃이 대량 발생함

개선 방법

  • 박스 너비를 읽어오는 부분을 분기분 바깥 쪽에서 수행하면 레이아웃 스래싱을 막을 수 있음
function resizeAllParagraphs(paragraphs) {
  const width = box.offsetWidth;

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

강제 동기 레이아웃을 일으키는 동작 정리

 

메모리 관리

  • 의도하지 않은 전역 변수 사용
  • 잊혀진 타이머
  • 참조된 DOM Node 삭제
  • 클로저 사용에 따른 누수

메모리 누수 사례 및 해결방법

의도하지 않은 전역 변수 사용

메모리 누수

  • 전역 함수 내부에서 선언문(ex: const) 없이 변수를 사용하는 경우 해당 변수는 전역에 선언됨
function foo(arg) {
    bar = "의도하지 않은 전역 변수";
}
  • 전역 변수에 값을 할당하면 해당 함수가 종료되더라도 전역에 값에 대한 참조가 남게 되어 메모리 누수 발생
function foo(arg) {
    this.bar = "이 경우도 전역에 선언됨";
}
foo();
  • 전역 함수 선언문 안에서 this를 사용하면 전역에 선언됨

해결 방법

  • use strict 사용하여 의도 되지 않은 전역 변수를 미연에 방지

잊혀진 타이머

메모리 누수

  • Javascript에서 타이머로 사용하는 setInterval 함수는 일반적으로 아래의 코드와 같이 사용함
let someResource = getData();
const interval = setInterval(function() {
    const someNode = document.getElementById('some-node');
    if(someNode) {
        someNode.innerHTML = JSON.stringify(someResource));
    }
}, 1000);
  • someNode 엘리먼트(id : 'some-node')가 유효할 경우에는 문제가 없음
  • 그러나 someNode 엘리먼트가 삭제될 경우 Interval 함수에서 사용하는 someResource는 불필요 함에도 불구하고 메모리 상에 남아있음

해결 방법

  • someNode가 삭제 될 경우 타이머 자체를 중단하여 불필요 메모리를 해제함
someResource = null; // IE7 이하 브라우저
clearInterval(interval);

참조된 DOM Node 삭제

메모리 누수

const elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};

function doStuff() {
    elements.image.src = 'http://some.url/image';
    elements.button.click();
    console.log(elements.text.innerHTML);
    // ...
}

function removeButton() {
    document.body.removeChild(document.getElementById('button'));
}
  • removeButton()을 수행할 경우 '#button'은 삭제되지만 '#button'에 대한 참조(elements.button)는 남아 있게 됨
  • 이 때문에 '#button'은 GC되지 않음

해결 방법

  • removeButton() 함수에서 #button 삭제 시 참조도 같이 삭제함
function removeButton() {
    document.body.removeChild(document.getElementById('button'));
    elements.button = null;
}

이중 클로저로 인한 메모리 누수

  • 이중 클로저 사용 시 메모리 누수가 발생할 수 있음
var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) {
      console.log("hi");
    }
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};
setInterval(replaceThing, 1000);
  • 매번 타이머 실행시 마다 새로운 함수가 실행
  • 과정 설명
    1. setInterval 1회 실행
      • [fn1]originalThing = null;
      • [fn1]theThing = {...};
    2. setInterval 2회 실행
      • [fn2]originalThing = [fn1]theThing;
      • [fn2]theThing = {...};
    3. setInterval 3회 실행
      • [fn3]originalThing = [fn2]theThing;
      • [fn3]theThing = {...};
    4. ...
    • 전제 조건
      • 실행 함수를 횟수에 따라 다음과 같이 표기하고 변수와 값에 prefix로 붙임
        • 첫번째 실행 함수 : [fn1]
        • 첫번째 실행 함수의 변수 a : [fn1]a
  • 타이머 실행으로 새로운 함수가 실행될 때 마다 이전 함수의 변수를 참조함

이중 클로저 메모리 누수 해결 방법

  • 타이머와 같이 반복적으로 매번 실행되는 함수에서 클로저 사용 시 이전 실행 함수의 변수를 참조하지 않도록 주의해야 함

누수 확인: 크롬 개발도구 > Memory 탭

  • Heap Snapshot
    • 페이지의 자바스크립트 객체와 관련된 DOM 노드 사이의 메모리 분포를 보여줌
    • 스냅샷끼리 비교해서 메모리 누수를 찾아낼 수 있음
  • Allocation instrumentation on timeline
    • 시간의 흐름에 따라 메모리 누수를 확인할 수 있음
  • Allocation sampling
    • 메모리가 할당된 Javascript 함수를 보여줌
728x90
반응형

+ Recent posts