크리티컬 렌더링 패스 최적화란, css, javascript, html을 적게 가져오고, 파싱 및 실행하여,
사용자의 인터랙션까지의 시간을 단축시키는 것을 의미한다.
The Document Object Model
모든 웹 페이지에는 Document Object Model 또는 DOM이 있습니다.
이것은 구문 분석(parsed)된 상태에 있는 전체 HTML 페이지의 객체 표현입니다.
HTML이 구문 분석되면 DOM 트리가 생성됩니다.
DOM 트리에는 객체가 포함되어 있습니다.
<html>
<head>
<link rel="stylesheet" href="style.css">
<title>101 Javascript Critical Rendering Path</title>
<body>
<header>
<h1>The Rendering Path</h1>
<p>Every step during the rendering of a HTML page, forms the path.</p>
</header>
<main>
<h1>You need a dom tree</h1>
<p>Have you ever come across the concepts behind a DOM Tree?</p>
</main>
<footer>
<small>Thank you for reading!</small>
</footer>
</body>
</head>
</html>
모든 브라우저는 위의 HTML을 구문 분석하는 데 시간이 걸립니다.
클린한 시멘틱 마크업은 브라우저가 HTML을 구문 분석하는 데 필요한 시간을 줄이는 데 도움이 됩니다.
CSSOM Tree
header{
background-color: white;
color: black;
}
p{
font-weight:400;
}
h1{
font-size:72px;
}
small{
text-align:left
}
Executing JavaScript
파서 차단이란 무엇입니까?
JavaScript 코드를 다운로드하고 실행해야 하는 경우, 브라우저는 DOM 트리 파싱 및 구성을 일시 중지합니다.
JavaScript 코드가 실행되는 순간 DOM 트리의 구성이 계속됩니다.
Let’s work on some real-time examples
- 순수한 HTML은 가져오기 및 구문 분석을 많이 포함하지 않습니다.
- 그러나 CSS 파일을 사용하면 CSSOM(위에서 설명한 대로)을 구성해야 합니다. HTML DOM과 CSS CSSOM이 모두 빌드되어야 합니다. 이것은 확실히 시간이 많이 걸리는 과정입니다.
참고: domContentLoaded는 HTML DOM이 완전히 구문 분석되고 로드될 때 시작됩니다.
이벤트는 이미지, 서브프레임 또는 스타일시트가 완전히 로드될 때까지 기다리지 않습니다.
유일한 대상은 문서를 로드하는 것입니다.
DOM이 파싱되어 로드되었는지 여부를 확인하기 위해 window 인터페이스에 이벤트를 추가할 수 있습니다.
이벤트 리스너는 다음과 같습니다.
window.addEventListener('DOMContentLoaded', (event) => {
console.log('DOM Content Loaded Event');
});
용어를 올바르게 정리합시다!
- Critical Resource : 페이지 렌더링을 차단할 수 있는 모든 리소스입니다.
- Critical Path Length : 페이지를 빌드하는 데 필요한 모든 중요한 리소스를 가져오는 데 필요한 총 왕복 횟수입니다.
- Critical Bytes : 페이지 빌드 완료 시 전송된 총 바이트 수입니다.
- 1 크리티컬 리소스 (html)
- 1 왕복 (1 Critical Path Length)
- 192바이트의 데이터 (Critical Byte)
순수 HTML과 외부 CSS 스크립트를 포함한 두번째 예에서는 다음과 같습니다.
- 2 크리티컬 리소스 (css, html)
- 2 왕복
- 400 바이트의 데이터
모든 프레임워크 혹은 일반 HTML+CSS+Javascript 코드에서 주요 렌더링 경로를 최적화하려면
위의 측정항목을 작업하고 개선해야 합니다.
- 가능한 한 적은 수의 크리티컬 리소스를 포함하는 것이 중요합니다. 이것은 CPU와 브라우저에 더 적은 작업을 부과합니다.
- 다운로드에 걸리는 시간과 리소스 크기 간의 종속성을 조심합시다
- . 리소스가 크면 크리티컬 패스 길이가 늘어납니다. 리소스를 가져오기 위한 더 많은 왕복 횟수가 필요합니다.
- 크리티컬 바이트를 최소한으로 유지하기 위해, 압축을 활용하거나, 중요하지 않은 리소스로 변경할 수 있습니다.
How to Cut Down on Render-Blocking Resources CSS?
모든 웹 페이지에는 초기 스크롤 지점(폴드) 이전과 이후 콘텐츠가 있습니다.
초기 컨텐츠를 주의 깊게 선택합니다.
이들은 크리티컬한 스타일입니다. 나머지 스타일은 나중에 로드할 수 있습니다.
즉, 펼치기 전에 필요한 스타일만 다운로드 하고, 이후 스타일은 나중에 받아도 됩니다.
이렇게 하면 웹 페이지의 속도를 높일 수 있습니다. 또한 불필요한 렌더링 차단 스타일을 제거할 수 있습니다.
How to Cut Down on Parser Blocking Resources?
Lazy Loading
- 페이지에 오버레이가 있다고 상상해보십시오.
- 페이지를 로드하는 동안 이 오버레이의 CSS, JavaScript 및 HTML을 로드하지 마십시오.
- 대신 버튼에 이벤트 리스너를 추가하고 사용자가 버튼을 클릭할 때만 스크립트를 로드합니다.
- 이 기능을 수행하려면 Webpack을 사용하십시오.
<img src="image.png" loading="lazy">
<iframe src="tutorial.html" loading="lazy"></iframe>
참고: loading=lazy를 사용한 지연 로딩은 첫 번째로 보이는 뷰포트 내의 이미지에 사용하면 안 됩니다.
접힌 상태의 이미지에만 적용해야 합니다.
- ".lazy" 클래스가 있는 모든 요소를 관찰합니다.
- ".lazy" 클래스가 있는 요소가 뷰포트에 있으면 교차 비율이 0 이상이 됩니다.
- 교차 비율이 0이거나 0 미만이면 대상이 시야에 없는 것입니다. 아무것도 할 필요가 없습니다.
- 이제 이러한 요소에 대해 미리 정의된 일련의 작업이 수행됩니다.
var intersectionObserver = new IntersectionObserver(function(entries) {
if (entries[0].intersectionRatio <= 0) return;
//intersection ratio is above zero
console.log('Loading Lazy Items');
});
// start observing
intersectionObserver.observe(document.querySelector('.lazy));
Async, Defer, Preload
Async를 사용하면 JavaScript 리소스가 다운로드되는 동안 브라우저가 다른 작업을 수행할 수 있습니다.
다운로드한 자바스크립트 리소스는 다운로드가 완료되는 즉시 실행됩니다.
- JavaScript는 비동기적으로 다운로드됩니다.
- 다른 모든 스크립트의 실행이 일시 중지됩니다.
- DOM 렌더링은 동시에 발생합니다.
- DOM 렌더링은 스크립트가 실행될 때만 일시 중지됩니다.
- 렌더링 차단 JavaScript 문제는 async 속성을 사용하여 해결할 수 있습니다.
“리소스가 중요하지 않다면 async도 쓰지 말고 완전히 생략하세요”
예시:
<p>...content before scripts...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM ready!"));
</script>
<script async src=""></script>
<!-- will be visible after the above script is completely executed –>
<p>...content after scripts...</p>
Defer를 사용하면 HTML 렌더링이 발생하는 동안 JavaScript 리소스가 다운로드됩니다.
- Defer는 async 이후에 실행됩니다.
- 스크립트 실행은 렌더링이 완료된 후에만 발생합니다.
- Defer는 JavaScript 리소스를 비 렌더링 차단 리소스로 만들 수 있습니다.
예시:
<>
<p>...content before script...</p>
<script defer src=""></script>
<!-- this content will be visible immediately -->
<p>...content after script...</p>
Preload : HTML 파일에는 필요하지 않지만, JavaScript 또는 CSS 파일을 렌더링하거나 구문 분석하는 동안 필요한 파일에 사용합니다.
Preload를 사용하면 브라우저가 리소스를 다운로드 하고, 우리는 리소스가 필요할 때 사용할 수 있습니다.
- Preload를 현명하게 사용하십시오. 브라우저는 페이지에서 필요하지 않은 경우에도 파일을 다운로드합니다.
- Preload가 너무 많으면 페이지 속도가 느려집니다.
- Preload된 파일이 너무 많으면 Preload를 사용하는 우선 순위가 영향을 받습니다.
- 접힌 내용 위에 필요한 파일만 Preload합니다.
- 이렇게 하면 Google PageSpeed Insight 점수가 높아집니다.
- 다른 파일이 렌더링될 때만 사용됩니다.
- 예를 들어 CSS 파일 내의 글꼴에 대한 링크를 추가합니다.
- CSS 파일이 구문 분석될 때까지 새 글꼴의 필요성을 알 수 없습니다.
- 이전에 글꼴을 다운로드했다면 사이트 속도가 향상됩니다.
- Preload는 오직 <Link> 태그와만 사용됩니다.
<link rel="preload" href="style.css" as="style">
<link rel="preload" href="main.js" as="script">
Vanilla JS로 작성하고 타사 스크립트를 피하십시오.
Caching, and Expiring Content
프론트엔드 코드의 모든 부분에서 캐싱을 달성하기 위해
브라우저는 HTTP 응답에서 네 가지 중요한 헤더 속성을 찾습니다.
- ETag
- Cache-Control
- Last-Modified
- Expires
Etag는 엔터티 태그라고도 합니다. 이것은 캐시 토큰의 유효성을 검사하는 문자열일 뿐입니다.
Cache-Control을 사용하면 애플리케이션이 주어진 요청에 대한 브라우저의 캐싱 정책을 결정할 수 있습니다.
Last Modified는 ETag와 매우 유사하지만 요청의 Last-Modified Header에 따라 다릅니다.
수정 날짜 및 시간은 클라이언트가 새 요청을 해야 하는지 여부를 결정하는 데 도움이 됩니다.
Expires는 데이터의 유효성을 결정하는 데 널리 사용되는 태그 중 하나입니다.
항상 애플리케이션은 만료되지 않은 데이터를 사용해야 합니다./*Install gets executed when the user launches the single page application for *the first time
*/
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(cacheName).then(function(cache) {
return cache.addAll(
[
'styles.css',
'script.js'
]
);
})
);
});
//When a user performs an operation
document.querySelector('.lazy').addEventListener('click', function(event) {
event.preventDefault();
caches.open('lazy_posts’).then(function(cache) {
fetch('/get-article’).then(function(response) {
return response;
}).then(function(urls) {
cache.addAll(urls);
});
});
});
//When there is a network response
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.open('lazy_posts').then(function(cache) {
return cache.match(event.request).then(function (response) {
return response
});
})
);
});
리액트의 관점에서 이야기해 봅시다.
Stage One
- Header
- Sidebar
- Footer
우리 애플리케이션에서는 사용자가 로그인한 경우에만 사이드바가 표시되어야 합니다.
Webpack은 코드 분할에 도움이 되는 훌륭한 도구입니다.
코드 분할을 활성화했다면 App.js 또는 Route 컴포넌트에서 바로 React Lazy 로딩을 사용할 수 있습니다.
그렇다면 지연 로딩은 무엇일까요?
이것은 코드를 논리적 조각으로 나누는 방법입니다.
논리적 조각은 애플리케이션이 필요할 때만 로드됩니다.
결과적으로 코드의 전체 무게는 작게 유지됩니다.
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
|- Header.js
|- Sidebar.js
|- Footer.js
|- loader.js
|- route.js
|- /node_modules
import { Switch, browserHistory, BrowserRouter as Router, Route} from 'react-router-dom';
const Header = React.lazy( () => import('Header'));
const Footer = React.lazy( () => import(Footer));
const Sidebar = React.lazy( () => import(Sidebar));
const Routes = (props) => {
return isServerAvailable ? (
<Router history={browserHistory}>
<Switch>
<Route path="/" exact><Redirect to='/Header’ /></Route>
<Route path="/sidebar" exact component={props => <Sidebar {...props} />} />
<Route path="/footer" exact component={props => <Footer {...props} />} />
</Switch>
</Router> : <></>
}
const Header = React.lazy( () => import('Header'));
const Footer = React.lazy( () => import(Footer));
const Sidebar = React.lazy( () => import(Sidebar));
function App (props) {
return(
<React.Fragment>
<Header user = {props.user} />
{props.user ? <Sidebar user = {props.user /> : null}
<Footer/>
</React.Fragment>
)
}
//Sidebar.js
export default () => {
console.log('You can return the Sidebar component here!');
};
import _ from 'lodash';
function buildSidebar() {
const element = document.createElement('div');
const button = document.createElement('button');
button.innerHTML = 'Login';
element.innerHTML = _.join(['Loading Sidebar', 'webpack'], ' ');
element.appendChild(button);
button.onclick = e => import(/* webpackChunkName: "sidebar" */ './sidebar).then(module => {
const sidebar = module.default;
sidebar()
});
return element;
}
document.body.appendChild(buildSidebar());
import React, { Suspense } from 'react';
import { Switch, browserHistory, BrowserRouter as Router, Route} from 'react-router-dom';
Import Loader from ‘./loader.js’
const Header = React.lazy( () => import('Header'));
const Footer = React.lazy( () => import(Footer));
const Sidebar = React.lazy( () => import(Sidebar));
const Routes = (props) => {
return isServerAvailable && (
<Router history={browserHistory}>
<Suspense fallback={<Loader trigger={true} />}>
<Switch>
<Route path="/" exact><Redirect to='/Header’ /></Route>
<Route path="/sidebar" exact component={props => <Sidebar {...props} />} />
<Route path="/footer" exact component={props => <Footer {...props} />} />
</Switch>
</Suspense>
</Router>
}
Stage Two
올바른 상태 관리 방법 사용
- React DOM 트리가 수정될 때마다 브라우저가 강제로 리플로우됩니다. 이는 애플리케이션 성능에 심각한 영향을 미칩니다.
- reconcilation은 라렌더링 횟수를 줄이는 데 사용됩니다.
- 마찬가지로 React는 상태 관리를 사용하여 리렌더링을 방지합니다.
- 예를 들어 useState() 후크가 있습니다.
- 클래스 구성 요소를 빌드하는 경우 shouldComponentUpdate() 수명 주기 메서드를 사용합니다.
- 항상 PureComponent를 확장하는 클래스를 생성하십시오.
- 그리고 shouldComponentUpdate() 후크는 PureComponent에서 구현되어야 합니다.
- 이렇게 하면 state와 props 간에 얕은 비교가 발생합니다.
- 따라서 다시 렌더링할 가능성이 크게 줄어듭니다.
React.Memo를 활용하세요
React.Memo는 컴포넌트를 파라미터로 받아 props를 메모합니다. 컴포넌트를 다시 렌더링해야 하는 경우 얕은 비교가 수행됩니다.function MyComponent(props) {
}
function areEqual(prevProps, nextProps) {
/*
return true if passing nextProps to render would return
the same result as passing prevProps to render,
otherwise return false
*/
}
export default React.memo(MyComponent, areEqual);
함수 컴포넌트를 사용하는 경우 useCallback() 및 useMemo() 를 사용하십시오.
Conclusion
이제 크리티컬 렌더링 패스가 무엇인지 알았으니, 작성한 코드를 분석해 보세요
프로젝트에 포함된 모든 라인, 모든 리소스 및 모든 파일은 크리티컬 렌더링 패스에 추가됩니다.
또한 웹 페이지의 폴드된 내용에 대해 생각하십시오.
웹사이트 성능 향상을 위한 팁과 요령을 활용하지 않았다면, 지금이 시작하기에 가장 좋은 시기일 수 있습니다.
성능은 모든 웹 애플리케이션에서 매우 중요합니다.
복잡성과 크기가 증가함에 따라 밀리초마다 차이가 발생합니다.
그럼에도 불구하고 조기 최적화는 재앙이 될 수 있음을 기억하십시오.
항상 성능을 측정한 다음 최적화 작업을 시도하십시오.
'FrontEnd' 카테고리의 다른 글
대규모 프로젝트 React Query 아키텍처 (1) | 2022.06.11 |
---|---|
리액트 라우터 v6(React Router v6) 딥 다이브 (0) | 2022.06.09 |
리액트 성능 최적화 : Death By a Thousand Cuts (천 번 베이면 죽는다.) (0) | 2022.06.05 |
리액트 성능 최적화 : Production Monitoring (0) | 2022.06.05 |
리액트 성능 최적화 : Virtual DOM (0) | 2022.06.05 |