프론트엔드 성능 최적화를 공부하다 보면 이런 이야기를 자주 듣게 된다.
처음엔 그냥 "최적화 팁"처럼 보이지만, 사실 이건 전부 JavaScript 엔진(V8)의 내부 최적화 방식과 연결되어 있다.
이번 글에서는
가 실제로 어떻게 동작하는지, 그리고 왜 React 성능과 연결되는지 정리해보려 한다.
(V8 기준으로 설명)
JavaScript는 기본적으로 동적 언어다.
즉 이런 코드가 있을 때
obj.a
엔진은 런타임마다
a 프로퍼티가 진짜 있는지확인해야 할 수도 있다.
원래라면 굉장히 느릴 수 있는 구조다.
그래서 현대 JS 엔진은 내부적으로 엄청난 최적화를 수행한다.
JavaScript
↓
Parser
↓
AST 생성
↓
Interpreter (Bytecode 실행)
↓
Profiler (실행 패턴 수집)
↓
JIT Compiler
↓
Optimized Machine Code
핵심은:
처음엔 빠르게 실행하고,
자주 쓰이는 코드를 나중에 최적화한다
는 점이다.
예전 JavaScript 엔진은
코드 한 줄 해석 → 실행
방식이었다.
즉 인터프리터 기반.
하지만 이 방식은 반복 실행되는 코드에서 비효율적이었다.
요즘 엔진은
흐름으로 동작한다.
이걸 JIT(Just-In-Time) 컴파일이라고 한다.
function add(a, b) {
return a + b;
}
처음엔 일반적인 방식으로 실행된다.
하지만 계속
add(1, 2);
add(3, 4);
add(5, 6);
처럼 숫자만 들어온다면 엔진은 판단한다.
"이 함수는 숫자 계산 전용이네?"
그 순간 JIT가 최적화를 시작한다.
원래 JS는 매 호출마다
같은 작업을 해야 한다.
하지만 숫자만 들어온다고 확신하면
타입 검사 생략
가능해진다.
즉 거의 네이티브 코드 수준으로 빨라진다.
갑자기
add("a", "b");
가 들어오면?
기존 최적화 가정이 깨진다.
엔진은
Deoptimization (deopt)
을 수행한다.
즉
가 발생한다.
JavaScript 엔진은
"예측 가능한 코드"
를 굉장히 좋아한다.
즉
이 중요하다.
이건 V8 최적화의 핵심 중 하나다.
JS 객체는 동적으로 구조가 바뀔 수 있다.
const user = {};
user.name = "me";
런타임에 프로퍼티가 계속 추가된다.
문제는 CPU는 원래
struct User {
char* name;
}
같은 고정 구조를 좋아한다는 점이다.
V8은 객체를 내부적으로
"고정 구조처럼"
관리한다.
예를 들어
const user = {
name: "me",
age: 20,
};
를 만들면 내부적으로
HiddenClass1
- name → offset 0
- age → offset 1
같은 구조를 생성한다.
user.name
접근 시
offset 0 접근
만 하면 된다.
즉 일반 해시 탐색이 아니라:
거의 C 구조체 접근처럼 동작한다.
const a = { x: 1, y: 2 };
const b = { y: 2, x: 1 };
겉보기엔 같아 보이지만
즉 엔진 입장에서는 서로 다른 객체 구조다.
예
const state = {
name,
age,
};
객체 구조가 계속 일정하게 유지되면 엔진 최적화가 잘 유지된다.
반대로
const obj = {};
if (condition) {
obj.name = "me";
}
처럼 구조가 계속 바뀌면 최적화가 깨질 가능성이 높아진다.
즉
객체 shape 안정성
이 성능과 연결된다.
이건 Hidden Class와 연결되는 최적화다.
user.name
를 매번
하면서 찾으면 느리다.
엔진은 캐싱한다.
예를 들어
"이 객체는 HiddenClass1 이었지?"
를 기억해둔다.
그러면 다음 접근부터는
바로 offset 접근
가능하다.
이걸 Inline Cache(IC)라고 한다.
즉
"이 형태의 객체는 여기 접근하면 된다"
를 캐싱하는 구조다.
처음 접근
lookup 발생
두 번째부터
cache hit
즉 훨씬 빨라진다.
foo(obj1);
foo(obj2);
foo(obj3);
근데 매번 객체 shape가 다르다면?
엔진 입장에서는
"왜 맨날 다른 객체가 들어오지?"
상태가 된다.
이를
Megamorphic
상태라고 부른다.
즉 Inline Cache가 너무 많은 형태를 처리해야 해서 최적화 효율이 떨어진다.
예를 들어
<Component style={{ color: "red" }} />
이 코드는 렌더마다
{ color: "red" }
새 객체를 생성한다.
즉
이 생긴다.
그래서
같은 패턴이 중요해진다.
items.map((item) => ({
...item,
active: true,
}));
렌더마다
가 발생한다.
물론 무조건 나쁘다는 건 아니지만:
자주 렌더되는 대규모 리스트
에서는 성능에 영향을 줄 수 있다.
일단 빠르게 실행
엔진이 관찰한다.
- 숫자만 오네?
- 객체 구조 일정하네?
- 호출 패턴 안정적이네?
객체 구조 최적화
프로퍼티 접근 캐싱
최적화된 머신코드 생성
타입이나 구조가 바뀌면
deopt 발생
다시 일반 모드로 복귀
JavaScript 엔진 최적화는 결국
"예측 가능한 코드"
를 좋아한다.
즉
이 성능 최적화의 핵심이다.
이걸 이해하면
를 단순 React 레벨이 아니라
"엔진 수준"
에서 이해할 수 있게 된다.