이번 포스팅에서는 웹의 최적화 기법과 구글에서 제공하는 최적화 도구인 LightHouse에 대해서 정리하겠습니다.
최적화는 한 마디로 가장 작은 자원으로 최고의 성능을 내는 것을 의미합니다. 웹 사이트의 경우 주어진 조건에서 가장 빠르게 페이지를 화면에 표시하는 것 입니다. 최적화를 통해서 사용자의 이탈률을 감소시키고 제공하는 서비스를 사용하게 함으로써 회사의 수익을 증대시킬 수 있는 꼭 필요한 작업 입니다.
최적화가 좋지 못할 경우, 사용자 경험이 좋지않기 때문에 컨턴츠가 좋은 서비스라도 사용자에게 외면 받게 될 것 입니다. 구글 조사 결과에 따르면 페이지 로드가 3초 이상되는 사이트는 50% 이상이 그 사이트를 이탈한다고 하니 서비스 운영에 있어서 웹 최적화가 얼마나 중요한지 크게 느낄 수 있었습니다.
브라우저는 HTML, CSS 파일을 토대로 DOM, CSSOM 트리를 만들어 결합한 후 화면을 렌더링 합니다. 두 트리 중 하나의 트리라도 변경이 생기면 리렌더링이 발생기 때문에, HTML, CSS 파일을 최적화하는 것만으로도 웹 페이지를 최적화할 수 있습니다.
DOM 트리가 깊을 수록 트리의 복잡도가 높아지기 때문에 불필요한 요소를 제거하는 것이 중요합니다. 아래의 코드처럼 불필요한 div 태그를 제거하는 것만으로도 최적화에 도움이 됩니다.
// 수정 전
<div>
<ol>
<li> first </li>
<li> second </li>
<li> third </li>
</ol>
</div>
// 수정 후 : 불필요한 div 요소 제거
<ol>
<li> first </li>
<li> second </li>
<li> third </li>
</ol>
HTML 요소에 인라인 스타일을 적용할 경우, 많은 리플로우를 발생 시켜 렌더링 완료 시점을 늦추기 때문에 사용하지 않는 것이 바람직 합니다.
DOM 트리와는 달리, CSS 파일의 모든 코드의 분석이 끝난 후에 CSSOM 트리가 생성되기 때문에 사용하지 않는 CSS 코드는 CSSOM 트리 생성을 지연 시킵니다.
셀렉터를 복잡하게 작성할 경우 스타일 계산이나 레이아웃 시간을 더 많이 소모하게 됩니다.
DOM 트리는 HTML 코드를 한 줄 한 줄 읽으면서 순차적으로 구성할 수 있지만, CSSOM 트리는 CSS 코드를 모두 해석해야 구성할 수 있습니다. 따라서 CSSOM 트리를 가능한 빠르게 구성할 수 있도록 HTML 문서 최상단에 배치하는 것이 좋습니다.
<head>
<link href="style.css" rel="stylesheet" />
</head>
HTML 코드 파싱 중에 script 태그를 만나는 순간 해당 스크립트가 실행되며, script 태그 이전까지 생성된 DOM까지만 접근할 수 있습니다. 따라서 script 코드를 HTML 코드 중간에 넣으면 의도하지 않은 화면이 표시될 수 있습니다.
또한 스크립트 실행이 완료되기 전까지 DOM 트리 생성이 중단되기 떄문에, JavaScript 파일을 다운받아와서 사용하는 경우에는 다운로드 및 스크립트 실행이 완료될 때까지 DOM 트리 생성이 중단되어 랜더링 완료 시간이 지연됩니다.
따라서, JavaScript 파일은 DOM 트리 생성이 완료되는 시점인 HTML 문서 최하단에 배치하는 것이 좋습니다.
script 태그에 defer 속성을 사용하면 HTML이 파싱되는 동안 JS 파일을 다운 받은 후에 HTML 파싱이 끝나면 적용시킵니다.
async 속성이 명시된 경우 브라우저가 HTML이 파싱되는 동안에도 스크립트가 실행됩니다.
<body>
<div>...</div>
...
// JavsScript 파일은 body 요소가 닫히기 직전에 작성하는 것이 가장 좋습니다.
<script src="script.js" type="text/javascript"></script>
</body>
페이지를 구성하는 파일들의 용량의 많은 부분을 이미지와 같은 미디어 파일이 차지하기 때문에 이미지 파일을 최적화하면 최적화에 큰 도움이 됩니다.
CSS의 background-position 속성을 사용해 이미지의 일정 부분만 클래스 등으로 구분하여 사용하는 방법입니다. 한번의 이미지 요청으로 다양한 이미지를 사용할 수 있기 때문에 로딩 시간을 감소 시킵니다.
아래의 사진은 네이버의 메인화면에 사용되는 스프라이트 이미지 파일입니다. 하나의 배경 이미지에서 width, height, background-position 속성에 따라 원하는 이미지를 사용할 수 있습니다.
아이콘을 사용할 때, 이미지를 사용하는 것보다 Font Awesom, React-icon과 같은 아이콘 폰트를 사용하면 전체 파일 용량을 감소시킬 수 있습니다.
이미지 최적화를 위해 전통적으로 사용하는 JPEG 또는 PNG 형식이 아닌 새롭게 등장한 이미지 포맷인 WebP 또는 AVIF를 사용하여 용량을 더욱 감소시킬 수 있습니다. 다만 아직 지원하지 않는 브라우저가 있어 호환되지 않는 다는 단점이 있습니다.
이러한 단점을 해결하기 위해서 HTML pictur 태그를 사용할 수 있습니다. 만약 접속한 브라우저에서 source 태그 내의 srcset에 정의한 WebP 포맷을 지원하지 않는다면 해당 source 태그는 무시하기 때문에, 호환이 되는 브라우저를 대상으로 최적화가 가능합니다.
<picture>
<source srcset="logo.webp" type="image/webp">
<img src="logo.png" alt="logo">
</picture>
CDN은 컨텐츠를 효율적으로 전송하기 위해서 요청을 보내는 곳에서 가장 가까운 데이터 센터에 있는 자료에서 데이터를 가져옵니다. 데이터 전송과정에서 거치는 서버의 갯수가 감소하기 때문에 로딩 속도가 빠릅니다. CloudFront, CloutFlare는 대표적인 CDN 서비스 입니다
트리쉐이킹은 나무를 흔들어 잔가지를 털어내듯이 불 필요한 코드를 제거하는 것을 의미합니다. 불필요한 코드나, 라이브라리를 제가하여 웹을 최적화할 수 있습니다.
웹팩 4버전 이상에서는 ES6 모듈(import, export)을 대상으로 기본적인 트리쉐이킹을 제공합니다.
프로덕트를 빌드하기전에 불필요하게 import된 라이브러리를 제거하면 최적화에 큰 도움이 됩니다. 라이브러리 전체를 import하는 것이 아니라 라이브러리 중에서 필요한 코드만 import해서 사용할 수 있습니다.
import react from 'react' // react 라이브러리 import(불필요한 코드도 불러옵니다.)
import { useState } from 'react' // useState만 import(필요한 코드만 불러옵니다.)
Babel은 자바스크립트 구형 브라우저에서도 호환이 가능하도록 ES5 문법으로 변환하는 라이브러리입니다. 이 때 ES5문법은 import를 지원하지 않기 때문에 commonJS 문법의 require로 변경시키게 되는데, require은 export되는 모든 모듈을 불러오기 때문에 트리쉐이킹이 불가능하게 됩니다.
이러한 문제를 해결하기 위해서 Barbelrc을 아래와 같이 수정해 주면 ES5로 변환하는 것을 방지해 줍니다. Babel이 변환해주지 않는 다면 어떻게 ES6 모듈을 구형 브라우저에서 적용시키는 것 일까요? 찾아본 결과 웹팩은 Babel 없이 ES6 모듈을 지원하는 것을 알 수 있었습니다.
[참고] https://webpack.kr/api/module-methods/
{
“presets”: [
[
“@babel/preset-env”,
{
"modules": false // true 값을 주면 항상 ES5 문법으로 변환 합니다.
}
]
]
}
아래의 코드는 배열을 변경시키는 sideEffect가 발생 합니다. 웹팩은 사이드 이펙트를 일으킬 수 있는 코드의 경우, 사용하지 않는 코드라도 트리쉐이킹 대상에서 제외시킵니다.
const crews = ['kimcoding', 'parkhacker']
const addCrew = function (name) {
crews.push(name)
}
package.json 파일에서 sideEffects를 설정하여 사이드 이펙트가 생기지 않을 것이라고 명시해주면 웹팩에서 sideEffect를 고려하지 않고 트리쉐이킹을 수행 합니다.
{
"name": "tree-shaking",
"version": "1.0.0",
"sideEffects": false
}
Boolen 값이 아닌 특정 파일을 설정할 수도 있습니다.
{
"name": "tree-shaking",
"version": "1.0.0",
"sideEffects": ["./src/components/NoSideEffect.js"]
}
ES5로 작성된 모듈의 경우에는 리쉐이킹이 적용되지 않기 때문에, 같은 기능의 ES6 지원 모듈을 사용하는 것이 좋습니다.
Lighhouse는 웹 사이트의 성능을 측정할 수 있는 도구입니다. 이 도구는 최적화의 방향성을 제공합니다.
크롬의 개발자도구에서 lighhouse 탭을 통해 사용할 수 있습니다. 구글 사이트에 접속한 후 검사를 실행 시켰습니다.
검사과 완료되면 각 항목별로 리포트를 보여 줍니다.
Lighthouse를 설치합니다. 이때-g 옵션을 사용하여 Lighthouse를 전역 모듈로 설치하는 것이 좋습니다.
// 다음의 명령어로 검사를 실행할 수 있습니다.
npm install -g lighthouse
// 다음의 명령어로 모든 옵션을 볼 수 있습니다.
lighthouse <url>
lighthouse --help
Lighthouse 노드모듈을 이용해 동적으로 프로그래밍하여 페이지 검사 리포트를 생성할 수도 있습니다. 이를 이용해 성능 테스트를 자동화할 수 있습니다.
Performance 항목에서는 웹 성능을 측정합니다. 화면에 콘텐츠가 표시되는데 시간이 얼마나 걸리는지, 표시된 후 사용자와 상호작용하기 까진 얼마나 걸리는지, 화면에 불안정한 요소는 없는지 등을 확인합니다.
Accessibility 항목에서는 웹 페이지가 웹 접근성을 잘 갖추고 있는지 확인합니다. 대체 텍스트를 잘 작성했는지, 배경색과 콘텐츠 색상의 대비가 충분한지, 적절한 WAI-ARIA 속성을 사용했는지 등을 확인합니다.
Best Practices 항목에서는 웹 페이지가 웹 표준 모범 사례를 잘 따르고 있는지 확인합니다. HTTPS 프로토콜을 사용하는지, 사용자가 확인할 확률은 높지 않지만 콘솔 창에 오류가 표시 되지는 않는지 등을 확인합니다.
SEO 항목에서는 웹 페이지가 검색 엔진 최적화가 잘 되어있는지 확인합니다. 애플리케이션의 robots.txt가 유효한지, meta 요소는 잘 작성되어 있는지, 텍스트 크기가 읽기에 무리가 없는지 등을 확인합니다.
PWA 항목에서는 해당 웹 사이트가 모바일 애플리케이션으로서도 잘 작동하는지 확인합니다. 앱 아이콘을 제공하는지, 스플래시 화면이 있는지, 화면 크기에 맞게 콘텐츠를 적절하게 배치했는지 등을 점수가 아닌 체크리스트로 확인합니다.
Lighthouse는 성능 측정에 총 6가지 메트릭을 사용합니다.
First Contentful Paint, 줄여서 FCP는 성능(Performance) 지표를 추적하는 메트릭입니다.
FCP는 사용자가 페이지에 접속했을 때 브라우저가 DOM 컨텐츠의 첫 번째 부분을 렌더링하는 데 걸리는 시간을 측정합니다. 즉 사용자가 감지하는 페이지의 로딩속도를 측정할 수 있습니다.
위 타임라인에서 FCP는 첫 번째 텍스트와 이미지 요소가 화면에 렌더링되는 두 번째 프레임에서 측정됩니다.
이때 FCP처럼 일부 콘텐츠의 첫 번째 렌더링 시점을 측정하는 것이 아닌 주요 콘텐츠 로딩이 완료된 시점을 측정하는 것을 목표로 한다면 Large Contentful Paint, 줄여서 LCP 지표로 확인할 수 있습니다.
Largest Contentful Paint, 줄여서 LCP는 뷰포트를 차지하는 가장 큰 콘텐츠(이미지 또는 텍스트 블록)의 렌더 시간을 측정합니다. 이를 이용해 주요 콘텐츠가 유저에게 보이는 시간까지를 가늠할 수 있습니다.
다음의 표를 기준으로 LCP 점수를 해석할 수 있습니다.
Speed Index는 성능(Performance) 지표를 추적하는 메트릭입니다. Speed Index는 페이지를 로드하는 동안 얼마나 빨리 컨텐츠가 시각적으로 표시되는 지를 측정합니다.
Lighthouse는 먼저 브라우저의 페이지 로딩과정을 각 프레임마다 캡쳐합니다. 그리고 프레임 간 화면에 보이는 요소들을 계산합니다. 그 후 Speedline Node.js module을 이용하여 Speed Index 점수를 그래프의 형태로 나타냅니다.
점수에 따라 다음의 기준으로 성능을 분류합니다.
Time to interactive, 줄여서 TTI는 페이지가 로드되는 시점부터 사용자와의 상호작용이 가능한 시점까지의 시간을 측정합니다.
TTI는 페이지가 완전히 상호 작용 가능하기까지의 시간을 측정합니다. 그 기준은 다음과 같습니다.
대부분의 사용자는 0.05초가 넘는 작업에는 응답이 올때까지 계속 키보드를 두드리거나 마우스를 클릭하기 때문에 페이지가 느리다고 인식합니다. 이를 개선하기 위한 지표가 TBT입니다.
Total Blocking Time, 줄여서 TBT는 페이지가 유저와 상호작용하기까지의 막혀있는 시간을 측정합니다. Lighthouse에서는 FCP와 TTI 사이에 긴 시간이 걸리는 작업들을 모두 기록하여 TBT를 측정합니다.
다음의 예를 통하여 TBT의 측정 기준을 살펴보겠습니다.
위 타임라인에는 5개의 작업이 있고 그 중 50ms(0.05초)를 초과하는 3개는 긴 작업으로 간주됩니다.
따라서 메인스레드에서 작업을 실행하는 데 소요된 총 시간은 560ms(0.56)초이지만 TBT로 측정되는 것은 345ms(0.345초)입니다.
Cumulative Layout Shift, 줄여서 CLS는 사용자에게 컨텐츠가 화면에서 얼마나 많이 움직이는지(불안정한 지)를 수치화한 지표입니다 .CLS는 사용자가 예상치 못한 레이아웃 이동을 경험하는 빈도를 수량화하므로 시각적 안정성을 측정할 때 중요한 사용자 중심 메트릭입니다.
페이지 콘텐츠의 예기치 않은 이동은 일반적으로 리소스가 비동기식으로 로드되거나 DOM 요소가 기존 콘텐츠 위의 페이지에 동적으로 추가되기 때문에 발생합니다. 원인은 알 수 없는 크기의 이미지나 동영상, 대체 크기보다 크거나 작게 렌더링되는 글꼴, 동적으로 크기가 조정되는 타사 광고 또는 위젯일 수 있습니다.
Lighthouse는 웹 성능 최적화 뿐만 아니라 웹 접근성, 웹 표준, SEO 관련 항목도 확인하고 해결책을 제시해줍니다
구글 사이트 Performance 분석 결과에 따른 Lighthoues의 권고사항에 대해서 알아보겠습니다.
이미지에 기본 너비와 높이를 명시 해야한다는 의미입니다. 그래야만 브라우저가 너비와 높이를 계산하고 이지미가 로드 되기전에 미리 충분한 공간을 할당합니다.
아래의 코드와 같이 기본 너비와 높이를 명시하는 것만으로 웹 최적화에 도움이 됩니다.
<img width="1600" height="900" src="your-image.jpg">
예를들어 img 태그의 높이 값을 auto로 명시할 경우에 브라우저는 원본 이미지의 너비나 높이를 알 수 없기 때문에 다음과 같은 현상이 발생 합니다. 그리고 레이아웃 이동이 발생하여 CLS를 증가시킵니다.
img{
width: 100%;
height: auto;
}
모바일 기기에서 화면을 로드할 때 고려해야하는 최적화입니다.
viewport meta tag가 없으면 휴대기기는 일반적인 데스크톱 화면 너비로 페이지를 렌더링하고 페이지를 다시 축소하기 때문에 페이지 사이즈를 읽기 어렵습니다.
viewport meta tag 설정을 통해 뷰포트의 너비와 배율을 제어하여 모든 기기에서 크기가 올바르게 조정되도록 할 수 있습니다.
다음과 같이 head 태그 안에 meta 태그를 추가하고 name과 content 속성을 명시하면 됩니다.
<!DOCTYPE html>
<html lang="en">
<head>
…
<meta name="viewport" content="width=device-width, initial-scale=1">
…
</head>
…
[참고 사이트] https://developer.chrome.com/docs/lighthouse/pwa/viewport/
Lighthouse는 Critical Request가 체이닝 되어 여러 요청이 호출되는 것을 막도록 권장하고 있습니다.
Critical Requests는 초기 뷰포트에 표시되는 리소스를 의미 하며 HTML, CSS, webfont, images, logo가 있습니다. 이 요소들은 최초에 뷰포트를 보여주는데 필수적인 요소이기 때문에 가장 먼저 로딩 되어야 합니다.
구글 사이트에서는 아래와 같이 CSS에서 font를 불러왔기 때문에 경고를 띄운 것을 알 수 있습니다. 중요한 요소들을 포함하는 체인 길이가 길어지면 페이지 로드가 느려지기 때문에 체이닝 수를 줄여서 성능을 개선하는 것이 좋습니다.
다음과 같은 방법으로 체이닝 수를 줄일 수 있습니다.
User Timing API를 사용한 JavaScript 성능에 대한 측정값을 제공합니다.
생성된 네트워크 요청 수와 페이지가 로드되는 동안 전송된 데이터 양을 분석한 결과를 제공합니다.
아래와 같이 LCP 요소에 대한 정보를 제공합니다.
아래와 같이 가장 큰 CLS에 대한 정보를 제공합니다.
아래와 같이 long main-thread tasks의 정보를 제공합니다.
페이지가 로드될 때마다 브라우저는 메인 스레드를 사용하여 페이지 콘텐츠 렌더링과 관련된 대부분의 작업을 처리하고 사용자 상호 작용을 처리합니다. 만약 오랜 기간동안 실행되는 JavaScript 파일은 구문 분석 및 실행될 때까지 브라우저에서 다른 작업을 처리하지 못하게 합니다.
아래의 그림을 보면 실행 기간이 긴 작업 A, B, E가 메인 스레드를 오랜기간 점유하면서 다른 작업을 실행하지 못하게해서 전체 페이지 로드에 영향을 미치는 것을 알 수 있습니다.
웹 성능을 향상시키기 위해서는 실행 기간이 긴 메인 스레드의 작업의 수를 최대한 줄이는 것이 중요합니다.