반응형
웹 페이지의 성능 최적화는 로딩최적화와 렌더링최적화 두 단계로 나뉜다.
로딩 최적화
브라우저 렌더링
5단계
- 파싱
- html을 해석해 DOM 트리 구축
- html에 포함되어 있거나 리소스로부터 다운 받은 css를 해석해 CSSOM 트리 구축
- 스타일 계산
- DOM에 CSSOM 정보 매칭 -> render 트리 그림
- script, meta, link 태그는 렌더링에 반영하지 않음
- CSS로 감춘 노드 역시 렌더 트리에 포함하지 않음
- DOM에 CSSOM 정보 매칭 -> render 트리 그림
- 레이아웃(=리플로우)
- 브라우저의 뷰포트 안에서 노드가 가져야 할 정확한 위치와 크기를 계산
- 객체의 정확한 크기와 위치를 파악하기 위해 루트부터 노드를 순회하면서 계산
- 레이아웃 결과로 각 노드의 정확한 위치와 크기값을 픽셀값으로 렌더 트리에 반영
- 페인트
- 레이아웃에서 계산된 값을 이용해 렌더트리의 각 노드를 화면 상의 실제 픽셀로 변환함
- 위치와 관계 없는 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
- link rel preload 사용
- 최신 브라우저에서만 사용 가능 (chrome 50~, firefox 56만(partial), safari 11.1~, iOS 11.3~, android 76)
<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>
리소스 최적화
- 브라우저, 사용자기준에서 모두 효과적
- 요청(Request) 줄이기
- 실제 리소스의 용량 줄이기
요청을 줄이는 방법
- 중복되거나 불필요 파일 제거
- 하나의 JS, CSS 파일 사용
- webpack을 이용한 번들링
- 단순한 concat도 의미가 있음
- HTML, CSS로 대체 가능한 이미지 제거(작은 이미지인 경우 base64 이미지도 방법)
- 이미지 Sprites
- 작은 이미지가 여러개 들어간 웹앱
- css의 backgound-position을 설정해 사용
- 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);
- 매번 타이머 실행시 마다 새로운 함수가 실행
- 과정 설명
- setInterval 1회 실행
- [fn1]originalThing = null;
- [fn1]theThing = {...};
- setInterval 2회 실행
- [fn2]originalThing = [fn1]theThing;
- [fn2]theThing = {...};
- setInterval 3회 실행
- [fn3]originalThing = [fn2]theThing;
- [fn3]theThing = {...};
- ...
- 전제 조건
- 실행 함수를 횟수에 따라 다음과 같이 표기하고 변수와 값에 prefix로 붙임
- 첫번째 실행 함수 : [fn1]
- 첫번째 실행 함수의 변수 a : [fn1]a
- 실행 함수를 횟수에 따라 다음과 같이 표기하고 변수와 값에 prefix로 붙임
- setInterval 1회 실행
- 타이머 실행으로 새로운 함수가 실행될 때 마다 이전 함수의 변수를 참조함
이중 클로저 메모리 누수 해결 방법
- 타이머와 같이 반복적으로 매번 실행되는 함수에서 클로저 사용 시 이전 실행 함수의 변수를 참조하지 않도록 주의해야 함
누수 확인: 크롬 개발도구 > Memory 탭
- Heap Snapshot
- 페이지의 자바스크립트 객체와 관련된 DOM 노드 사이의 메모리 분포를 보여줌
- 스냅샷끼리 비교해서 메모리 누수를 찾아낼 수 있음
- Allocation instrumentation on timeline
- 시간의 흐름에 따라 메모리 누수를 확인할 수 있음
- Allocation sampling
- 메모리가 할당된 Javascript 함수를 보여줌
728x90
반응형