[프론트엔드 성능 최적화] LIVID STUDY 2주차 내용 정리

이주영·2023년 7월 19일
1

성능 최적화

목록 보기
2/4
post-thumbnail

들어가기 앞서 🌱

이번주는 여러모로 만족스러운 한주다. 주도적인 학습과 운동 그리고 프로젝트 모두 재밌고 탄력이 붙는 느낌이 든다. 이번 주는 올림픽 웹 서비스 예제를 활용하여 각자 실습을 해보고 토론하는 방식으로 이루어졌고 총 7명의 스터디원 중 딩코님께서 다양한 방면의 질문 보따리를 풀어주셔서 더할 나위없이 즐겁고 감사하다. 스터디 중 나온 질문들 중 몇가지를 꼽아 블로그에 작성해야겠다. 분명 프론트엔드 취준생분들에게 도움이 될 것이라고 생각한다.

2장 내용 정리

📕 중요 포인트

  1. CSS 애니메이션 최적화
  2. 컴포넌트 지연 로딩 및 사전 로딩
    • 1장을 통해 기본적인 개념을 알게 됐다. react 안에서 dynamic import를 사용하여 구현할 수 있다. 이번에도 마찬가지로 코드 자체를 분할하는 것이 아닌 컴포넌트를 분할하여 컴포넌트가 사용되는 시점에 import 한다.
  3. 이미지 사전 로딩
    • 이미지도 필요한 시점보다 먼저 import하는데 여기서 재밌었던 것은 useEffect 훅 내부에 서 시점을 조절한다는 것이었다.
  4. 브라우저 렌더링 과정

애니메이션 최적화

교제에서는 가로 막대가 늘어날때 부드럽지 않다고 하는데 왜 부드러운지 모르겠다. 아하! CPU 6x slowDown으로 설정해보니, jank (끊어져 보이는) 현상이 보인다.

❓애니메이션의 원리

사전 지식: 브라우저 렌더링 과정을 알아야 애니메이션의 원리를 이해하는데 도움이 된다.

핵심은 일반적으로 사용하는 디스플레이는 60Hz이다. 즉 1초에 60장의 화면을 빠르게 보여준다는 의미인데,브라우저도 이에 맞춰져있다고 한다.

브라우저 렌더링 과정을 간략하게 정리하면

  1. 서버로부터 html,css 리소스 로드하고 브라우저의 렌더링 엔징이 파싱하여 DOM과 CSSOM을 만든다. 두 개의 객체를 합쳐 렌더트리를 만든다. 만들어진 렌더트리로 각 요소의 레이아웃을 계산하는데 쓰인다.

  2. 렌더 트리가 완성되면 화면에 구성해야할 요소들의 위치를 계산한다. 이 과정을 layout이라고 한다.

  3. 계산이 끝나면 색을 채워넣는 과정이 진행된다. 이것은 paint.

  4. 마지막으로 각 레이어를 합치는 과정이 있다. composite!!

즉 정리해보면

DOM + CSSOM(렌더링 엔진) -> render Tree -> layout -> paint -> composite

더 나아가 중요한 개념이 있다.

  1. 리플로우 : 렌더링 과정을 다시 실행하게 하는 것. 위에서 정리한 모든 과정을 동일하게 실행하는 것.
  2. 리페인트 : 배경색과 같은 색상 속성의 변경으로 레아아웃 이후 페인트와 합성 단계만 거치게 하는 것.

애니메이션 최적화 부분을 다루고 있는 만큼 리플로우와 리페인트를 최소화하기 위해 취할 수 있는 방법은 없을까?

리플로우와 리페인트를 해결하기 위해서 GPU를 이용하면 된다고 한다!!

transform과 opacity와 같은 속성을 사용하면 된다. 이런 속성은 브라우저 단에서 작업을 GPU로 위임해서 처리해서 레아아웃과 페인트 단계를 뛰어 넘을 수 있다. 이 개념을 하드웨어 가속이라고 한다.

어..? 하드 웨어 가속이란?

CPU가 GPU에게 어떤 처리를 위임하는 방법을 말한다.

하드웨어 가속은 프로그램이 지원하는 컴퓨터의 하드웨어를 사용하여 소프트웨어에서 수행할 수 있는 것보다 더 효율적으로 일부 기능을 수행하는 경우를 말합니다. 하드웨어는 CPU 단독으로 실행되는 소프트웨어보다 일부 기능을 더 빠르게 수행하도록 설계되었습니다.
출처: 비디오바이트 공식문서

css 애니메이션 성능에 도움 되는 것

  1. transform과 opacity

    transform : translate()은 처음부터도 레이어를 분리하지 않고 변화가 일어나는 순간 레이어를 분리한다.
    transform : translate3d() 혹은 will-change 속성은 처음부터 분리해서 변화에 더욱 빠르게 대처할 수 있다.

만약 width로 애니메이션을 줄 경우 아래와 같이 계속 리플로우가 발생해서 정크 현상이 생긴다.유저의 네트워크 상황에 따라 끊길 수 있다.

스크린샷 2023-07-15 오후 12 07 36
도움이 되지 않는 것

width, height, color 왜냐하면 위에서 봤듯이 브라우저 렌더링 단계에서 layout 및 paint 단계에 반영되는 요소이기에 reflow와 repaint를 일으킨다고 생각하면 이해가 되지 않을까 싶다.

그렇다면 어떻게 width를 수정할까?

  1. transform의 다양한 값 사용
    1. 위치 이동은 translate 속성으로
    2. 크기 변경은 scale 속성으로
    3. 요소 회전은 rotate 속성으로

예를 들면 아래와 같은 코드를 수정하려면

transform: scaleX($ {({width}) => width / 100});
transform-origin: center left;

여기서 교제에서 알려준 하나의 팁은 scale의 기준점은 항상 center가 기본이다. 그래서 left로 옮겨줘야한다.

	//전
    width: ${({width}) => width}%;
    transition: width 1.5s ease;

    //후
	width: 100%;
    transform: scaleX(${({width}) => width/100});
    transform-origin: center left;
    transition: transform 1.5s ease;
스크린샷 2023-07-15 오후 12 07 54

네트워크 패널으로 분석해본 결과 width로 애니메이션을 부여했을때는 reflow가 일어나는 것을 확인했다. transform으로 애니메이션을 수정해보니 아래와 같이 놀라운 결과를 볼 수 있었다. 알리고 올리고 프로젝트에 적용해봐야지!

컴포넌트 지연 로딩

모달 코드 분리하기

리액트 코드에서 suspense 와 lazy 함수를 활용하여 동적으로 import하여 컴포넌트 로딩을 지연하였다. 그래서 생긴 문제는 컴포넌트가 마운트될때 js를 로드하기에 아래와 같이 약 1초에서 느리면 2초까지 아무일도 일어나지 않다가 모달이 뜨는 문제가 발생했다.

컴포넌트 사전 로딩

그럼 사전에 로딩하는 기점은 어떻게 선정할 것인지가 중요한 문제 그래서 책에서 추천하는 방식은 두가지가 있다. 첫번째는 유저가 해당 DOM에 mouse가 enter되었을때 파일을 로드하도록 하는것. 두번째는 컴포넌트가 마운트가 끝나면 그때 다른 파일을 로드하도록 하는 것.

버튼 위에 마우스를 올려놓았을 때 로딩

	const handleMouseEnter = () => {
		const component = import("./components/ImageModal");
	};

<button
	onMouseEnter={handleMouseEnter}
>

컴포넌트의 마운트 완료 후 사전 로딩

useEffect(() => {
	const component = import("./components/ImageModal");
}, []);
스크린샷 2023-07-15 오후 12 08 12

이미지 사전 로딩

이미지는 화면에 그려지는 시점 즉 html 혹은 css에서 이미지를 사용하는 시점에 로드된다. Js를 활용하여 이미지를 직접 로드할 수 있는 방법이 있다. 바로 Image 객체를 사용하는 것.

chart.js를 사용할때 사용해본 경험이 있는데 프로젝트에 적용해보자

개인프로젝트에 적용해보자

알리고 올리고라는 취준생들을 위한 목표 달성 웹서비스 내에 target, 즉 목표를 만드는 퍼널이 있다. 그 안에서 단계를 나타내기 위해 Progress Bar를 만들었다. 너무나 감사하게 과거의 나는 width를 활용해서 Bar를 만들었고 바로 적용해보려고 한다. 현재 보여지는 UI는 아래와 같다.

UI

배경 코드

import { useState } from "react";
//... 수많은 import 

const TargetCreate = () => {
	const targetService = useTarget();
	const navigate = useNavigate();
	const [message, setMessage] = useState("");
	const [step, setStep] = useState<TargetStepType>("goal");

	const methods = useForm({
		defaultValues: {
			subGoal: [{}, {}, {}],
			routine: [{}],
		},
		resolver: yupResolver(targetSchema),
	});

	const onSubmitHandler = (data: TargetInfoType) => {
		targetService
			?.postTarget(data)
			.then((res) => {
				navigate("/target");
			})
			.catch((error) => setMessage(error.APIMessage));
	};

	return (
		<div className=" flex flex-col items-center h-screen px-6 pb-10 relative">
			<FormProvider {...methods}>
				<form
					onSubmit={methods.handleSubmit(onSubmitHandler)}
					className="w-full relative"
				>
					<CreateBar step={step} /> // 이부분을 살펴볼 예정이다. 
					<Step check={step === "goal"}>
						<Goal setStep={setStep} />
					</Step>
					<Step check={step === "subGoal"}>
						<SubGoalRoutine setStep={setStep} />
					</Step>
					<Step check={step === "duration"}>
						<Duration setStep={setStep} />
					</Step>
					<Step check={step === "lastStep"}>
						<LastStep setStep={setStep} />
					</Step>
				</form>
			</FormProvider>
		</div>
	);
};

export default TargetCreate;


import { LoudOli } from "../../../utils/constant/image";

type Props = {
	step: keyof typeof getPercentFormStep;
};
const getPercentFormStep = {
	goal: `w-[25%]`,
	subGoal: `w-[50%]`,
	duration: `w-[75%]`,
	lastStep: `w-[100%]`,
};

const CreateBar = ({ step }: Props) => {
	return (
		<div className="py-4 px-4 sticky top-0 bg-white overflow-x-hidden overflow-y-hidden">
			<div className="h-2 w-full bg-[#e0e0de] rounded-md relative">
				<div
					className={`h-full bg-main rounded-md text-xs ${getPercentFormStep[step]} origin-left transition-all duration-300`}
				></div>
			</div>
		</div>
	);
};

export default CreateBar;

import { LoudOli } from "../../../utils/constant/image";

type Props = {
	step: keyof typeof getPercentFormStep;
};
const getPercentFormStep = {
	goal: `scale-x-25`,
	subGoal: `scale-x-50`,
	duration: `scale-x-75`,
	lastStep: `scale-x-100`,
	done: `scale-x-100`,
};

const getLocationFromStep = {
	goal: `translate-x-1/4`,
	subGoal: `translate-x-1/2`,
	duration: `translate-x-3/4`,
	lastStep: `translate-x-full`,
	done: `scale-x-100`,
};

const CreateBar = ({ step }: Props) => {
	return (
		<div className="py-4 px-4 sticky top-0 bg-white overflow-x-hidden overflow-y-hidden">
			<div className="h-2 w-full bg-[#e0e0de] rounded-md relative">
				<div
					className={`h-full bg-main rounded-md text-xs ${getPercentFormStep[step]} origin-left transition-transform duration-300`}
				></div>
				<div className={`${getLocationFromStep[step]}`}>
					<img
						src={LoudOli}
						alt="oli"
						className={`absolute h-8 -left-5 transform -translate-y-1/2 z-10 transition-transform duration-300`}
					/>
				</div>
			</div>
		</div>
	);
};

export default CreateBar;


분석해보자!

우선 Performance 패널에서 network와 CPU를 각각 낮춰주고 분석을 시작했다.

1. 이벤트 중 reflow repaint 관점에서 분석해보기

  • width 애니메이션을 활용한 코드에서 버튼을 클릭했을때

  • transition 애니메이션을 활용한 코드에서 버튼을 클릭했을때

놀랍게도, 확연한 차이가 눈에 보인다. 우선 width로 애니메이션을 만들었을때는 위치가 변경되면 layout과정이 계속 반복된다. 아주 작은 프로젝트인 알리고 올리고에서도 이렇게 눈에 보이는데 큰 프로젝트였다면 유의미한 성능 차이를 보일 것이다.

아래는 transition을 활용했고 GPU에게 위임했기에 널널해보인다.

2. 서머리를 통해 동일한 이벤트일 경우 rendering과 painting 비교하여 분석해보기

최대한 동일한 조건으로 테스트하기 위해 버튼이 클릭되고 애니메이션이 끝나는 시점의 Summary를 비교해보려고 했지만 한시간 내내 분석해봐도 시원한 결과는 나오지 않았다. transition이 확실히 width보다는 painting은 덜 되는 것으로 보이나 다른 부분에서 시간이 더 소요돼, 결과는 비슷한데 조금 더 알아봐야겠다.

프로젝트에 적용한 결과!

생각보다 유의미한 결과를 얻지 못했다... 죄송합니다 하지만 분명 transition으로 만든 애니메이션이 repaint의 빈도가 현저히 낮았다. 분석을 하기 위해서 동일한 환경에서 분석을 해야하는데 여러가지의 변수가 있어 분석하는데 어려움이 있었다. transition일때 왜 JS 파일이 더 큰지?등 아마 분명 다른 이유가 있겠지만! 오늘은 프로젝트에 적용해보면서 2주차 스터디를 통해 알게 된 CSS 애니메이션 최적화를 적용해보았다.

마치며 🌱

개인 프로젝트에 적용해보면서 머리로 알던 것들을 깨닫는 재미가 솔솔하다. 확실히 여러 명이 같이 토론의 형태로 여러 가지 주제에 대해 깊이 이야기해볼 수 있어서 역시나 유익하고 재밌다. 이번주는 시간이 너무 빨리 지나가버려 정신차려보니 1시간 30분이 지나 아쉬웠다. 

출처

  1. 프론트엔드 성능 최적화 가이드
  2. LIVID STUDY GIT REPO
profile
https://danny-blog.vercel.app/ 문제 해결 과정을 정리하는 블로그입니다.

2개의 댓글

comment-user-thumbnail
2023년 7월 19일

정말 좋은 글이었어요, 감사합니다.

1개의 답글