속도가 느린 웹사이트의 사용자 이탈율은 높을 수 밖에 없습니다. 그렇기에 웹사이트 속도 향상을 위한 노력은 개발자의 숙명과도 마찬가지입니다.
속도 성능 향상을 위해서는 여러가지 방법들이 존재하지만 이번 시간은 그 중 일부인 lazy loading, code splitting, suspense, automatic batching에 대해 알아보고자 합니다 :)
웹사이트 페이지의 리소스를 필요할 때 불러와주는 친구!
사용자가 페이지를 읽어들이는 시점에 필요한 리소스들이 우선 로드가 되고, 그 외 리소스들은 사용자가 필요로 할 때 추후에 로드하는 방법을 의미.
일반적으로 웹 페이지가 열리면 페이지 전체가 렌더링 되어 모든 내용이 다운로드 됨. 브라우저가 웹 페이지를 캐싱한다는 점은 좋지만 다운로드 된 전체 내용을 사용자가 모두 확인할지는 미지수. 사용자가 확인하지 않는 리소스를 다운로드 한다는건 데이터 및 메모리 낭비. 하지만 사용자가 다운로드를 요청하는 일종의 행위를 통해 필요할 때 리소스를 다운로드한다면 이러한 낭비를 방지할 수 있음.
컨텐츠 버퍼링과 랜더링 속도에 영향을 줄 수 있음
: 지연 로딩의 일례인 무한 스크롤의 경우, 화면 맨 아래의 element가 감지되면 데이터를 불러오는데 리소스가 아직 다운로드 중일 때 사용자가 스크롤하게 되면 컨텐츠 로딩에 버퍼링이 발생. 이 경우 랜더링 속도에 안 좋게 작용할 수 있음.
느린 이미지 로딩 속도
: 초기 웹 사이트를 로딩할 때 필요한 리소스만 다운로드 하기 때문에 추후 사용자가 필요로하는 리소스의 경우 페이지 레이이웃에 적합한 콘텐츠 크기를 가늠할 수 없음. 따라서 지연 로딩 이미지 랜더링 과정에서 로딩 속도 지연이 발생할 수 있음.
상대적으로 덜 중요한 콘텐츠에서 사용
: 모든 플랫폼에서 지연로딩을 제공하지 않는 점을 고려하여, 지연 로딩 사용시 에러 처리가 필수. 예상치 못한 역효과 발생시 사용자에게 좀더 나은 사용자 경험을 제공하도록 상대적으로 웹사이트에서 중요도가 낮은 콘텐츠에 지연 로딩 사용하기.
검색엔진 최적화(SEO; Search Engine Optimization)에서는 지양
: 웹사이트 페이지의 데이터를 수집하고 분류(크롤링)하는 SEO에서는 일부 리소스만 로드하는 지연 로딩이 좋지 않은 영향을 줄 수 있음. 따라서 이 역시 중요하지 않은 컨텐츠에 지연 로딩 적용하기.
import {lazy} from 'react';
const LazyLoading = lazy(()=> import ('./LazyLoading.js'));
<img alt="A lazy image" class="lazy" data-src="lazy.jpg" />
지연 로딩과 함께 쓰이기도 하는 서스펜스 !
비동기 작업 방식 중 하나로, 한 컴포넌트가 비동기적인 작업이 끝날 때까지 기다려야 할 때 해당 컴포넌트를 대신하여 다른 컴포넌트를 먼저 보여주고 비동기 작업이 끝나면 해당 컴포넌트를 보여주는 처리 방식.
const ProfilePage = React.lazy(() => import('./ProfilePage));
<Suspense fallback={<Spinner />}>
<ProfilePage />
</Suspense>
ProfilePage 컴포넌트는 지연 로딩이 사용된 컴포넌트로, 비동기적인 작업이 완전히 끝날때까지 기다려야하는 상황.
ProfilePage이라는 자식 컴포넌트 비동기 작업이 처리되는 동안 서스펜서의 fallback이라는 값으로 주어진 Spinner 컴포넌트가 랜더링 됨.
리액트는 서스펜스의 자식 컴포넌트를 감지하고 있다가 해당 컴포넌트의 비동기 처리가 완료되면 리액트는 fallback 값의 Spinner가 아닌, 원래 노출하고 싶었던 컴포넌트로 리랜더링 수행.
즉, 서스펜스 컴포넌트는 비동기적인 컴포넌트를 자식으로 두며 그 자식 컴포넌트가 비동기적 작업을 진행하는 동안 fallback에 할당된 컴포넌트를 랜더링함. 그리고 리액트에게 비동기 작업하는 자식 컴포넌트를 알려줌. 비동기 작업이 끝나면 자식 컴포넌트를 랜더링하도록 알려줌.
function ProfilePage() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(u => setUser(u));
}, []);
if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline />
</>
);
}
function ProfileTimeline() {
const [posts, setPosts] = useState(null);
useEffect(() => {
fetchPosts().then(p => setPosts(p));
}, []);
if (posts === null) {
return <h2>Loading posts...</h2>;
}
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
코드 출처
위와 같이, ProfilePage 함수의 fetchUser()라는 비동기적 작업으로 인해, ProfileTimeline 함수의 posts의 초기 리턴값인 'Loading posts...'가 보여지지 않아 사용자가 화면을 확인할 때 제한된 정보로 답답함을 느낄 수 있음. 이에 대한 해결책으로 아래와 같이 비동기적 작업들을 동시에 진행하는 방식 등장.
function fetchProfileData() {
return Promise.all([
fetchUser(),
fetchPosts()
]).then(([user, posts]) => {
return {user, posts};
})
}
하지만 이 방법 역시 fetchUser와 fetchPosts 둘중 한 작업이라도 시간이 걸리면 나머지 한 작업에 대한 데이터를 사용자가 기다려야 한다는 한계가 존재함. 이러한 한계에 대한 해결책으로 서스펜스 등장.
const resource = fetchProfileData();
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
</Suspense>
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
);
}
function ProfileDetails() {
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline() {
const posts = resource.posts.read();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
코드 출처
세스펜스는 비동기 작업들이 서로의 순서와 관계없이 랜더링 될 수 있도록 해줌. 먼저 실행되는 비동기 작업의 패칭을 시작하고, 그에 대한 대안의 fallback 값을 랜더링 한 후, 비동기 작업이 끝나면 비로소 해당 컴포넌트의 데이터를 랜더링 함.
서스펜스 역할 ? 기존의 waterfall 방식처럼 데이터 패칭 순서를 기재된 순서인 위에서부터 아래로 기다려야하는 방식이 아닌, 여러 비동기 작업들 중 먼저 끝나는 작업의 데이터를 먼저 보여줌!
서스펜스는 리액트에 자식 컴포넌트가 비동기적인 컴포넌트임을 알려줘야 함. 때문에 해당 컴포넌트는 비동기 데이터를 불러오는 함수를 써야하고, 해당 함수는 "특이한 형태"의 객체를 반환해야 함.
export const createResource = (promise) => {
let status = "pending";
let result;
let suspender = promise.then(
(data) => {
status = "success";
result = data;
},
(err) => {
status = "error";
result = err;
}
);
return {
read() {
if (status === "pending") {
throw susender;
} else if (status === "error") {
throw result;
}
return result;
},
};
};
애플리케이션을 필요한 시점에 로드할 수 있도록 번들(기능별로 모듈화 한 파일들을 하나로 묶는 작업)한 여러 코드 또는 컴포넌트를 분리하는 것. 코드 분할은 리액트 뿐만 아니라 웹팩을 다루는 다른 어플리케이션에서도 모두 사용 가능한 언어.
자바스크립트에서 빌드되는 과정에서 모든 파일들이 하나의 소스 코드로 합쳐지게 됨. 복잡한 SPA 프로젝트에서 네트워크 환경이 좋지 못할 경우 소스 코드를 로드하는데에 많은 시간이 필요. 이를 방지하기 위해 당장 필요한 코드만 로딩하고, 그 외 코드는 분리하여 필요할 때 로딩함으로써 로딩시간을 개선하고자 코드 분할 사용.
("빌드"란 ? 컴파일된 코드를 실행할 수 있는 상태로 만드는 일)
사용자가 웹사이트 초기 접속 시 다운로드하고 파싱하는 데이터 크기가 줄어 성능 향상 가능.
리액트는 아래의 내장된 메커니즘을 통해 코드 분할과 지연 로딩 제공.
React가 더 나은 성능을 위해 여러개의 state 업데이트를 한 번의 리렌더링으로 묶어서 진행하는 것.
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const decrease = async () => {
await delay(1500);
setTeamA((a) => a - 1);
setTeamB((b) => b - 1);
};
<button onClick={decrease}>1씩 줄이기</button>
import { createRoot } from "react-dom/client";
const root = createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
https://yozm.wishket.com/magazine/detail/1705/
https://www.youtube.com/watch?v=aCgaVfWMRBw
https://scarlett-dev.gitbook.io/all/it/lazy-loading
https://jokerkwu.tistory.com/199
https://17.reactjs.org/docs/concurrent-mode-suspense.html
https://www.youtube.com/watch?v=8q7OQSPLF4k
https://bamtory29.tistory.com/entry/React-%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%97%90%EC%84%9C-%EC%BD%94%EB%93%9C-%EC%8A%A4%ED%94%8C%EB%A6%AC%ED%8C%85
https://legacy.reactjs.org/docs/code-splitting.html
https://developer.mozilla.org/ko/docs/Glossary/Code_splitting
https://wikidocs.net/197644
https://nukw0n-dev.tistory.com/33
https://pannchat.tistory.com/entry/React-18-%EC%9E%90%EB%8F%99-%EB%B0%B0%EC%B9%AD-Automatic-batching