| title | impact | impactDescription | tags |
|---|---|---|---|
| Layout Thrashing 방지 | MEDIUM | forced synchronous layouts 방지, 성능 병목 감소 | javascript, dom, css, performance, reflow, layout-thrashing |
Layout Thrashing 방지
style writes와 layout reads를 interleaving하지 마세요. style 변경 사이에 layout properties를 읽으면, 브라우저가 synchronous reflow를 트리거해야 하므로 성능 병목이 발생합니다.
Incorrect (layout thrashing 발생):
function updateElements(elements: HTMLElement[]) {
elements.forEach(el => {
el.style.width = "100px"; // write
const height = el.offsetHeight; // read - forces reflow!
el.style.height = `${height * 2}px`; // write
});
}Correct (reads와 writes 분리):
function updateElements(elements: HTMLElement[]) {
// 먼저 모든 값 읽기
const heights = elements.map(el => el.offsetHeight);
// 그 다음 모든 styles 쓰기
elements.forEach((el, i) => {
el.style.width = "100px";
el.style.height = `${heights[i] * 2}px`;
});
}CSS Classes 사용 (더 나은 방법):
function updateElements(elements: HTMLElement[]) {
elements.forEach(el => {
el.classList.add("expanded");
});
}핵심 패턴:
- writes를 함께 batching - 브라우저가 reflow 계산을 최적화할 수 있음
- read와 write phases 분리 - 모든 layout measurements를 먼저 수행, 그 다음 style 변경 적용
- inline styles 대신 CSS classes 사용 - 브라우저 caching 활용, layout thrashing 완전 방지
React 애플리케이션에서는 useEffect hooks에서 inline styles를 조작하는 대신 className props를 통해 CSS classes를 토글하는 것을 선호하세요.
| title | impact | impactDescription | tags |
|---|---|---|---|
| 반복되는 함수 호출 캐싱 | MEDIUM | 중복 계산 방지 | javascript, caching, memoization, performance |
반복되는 함수 호출 캐싱
렌더링 중 동일한 inputs가 반복적으로 발생할 때 중복 함수 실행을 방지하기 위해 module-level caching을 사용하세요.
Incorrect (캐싱 없이 동일 inputs로 100+ 번 호출):
function ProjectList({ projects }: Props) {
return (
<ul>
{projects.map(p => (
<li key={p.id}>
{/* slugify가 동일한 이름으로 여러 번 호출될 수 있음 */}
<a href={`/project/${slugify(p.name)}`}>{p.name}</a>
</li>
))}
</ul>
);
}Correct (Map 기반 caching):
const slugCache = new Map<string, string>();
function slugify(name: string): string {
if (!slugCache.has(name)) {
// 비용 큰 계산
const slug = name.toLowerCase().replace(/\s+/g, "-");
slugCache.set(name, slug);
}
return slugCache.get(name)!;
}단일 값 caching (output이 하나일 때):
let currentUser: User | null = null;
function getCurrentUser(): User | null {
if (currentUser === null) {
currentUser = fetchUserFromSession();
}
return currentUser;
}
// 데이터가 변경되면 cache 초기화
function onLogout() {
currentUser = null;
}핵심 원칙: hook이 아닌 Map을 사용하여 어디서든 작동하게 하세요: utilities, event handlers, React components 외부에서도 사용 가능합니다.
| title | impact | impactDescription | tags |
|---|---|---|---|
| Loops에서 Property Access 캐싱 | LOW-MEDIUM | lookups 감소 | javascript, loops, optimization, caching |
Loops에서 Property Access 캐싱
hot paths에서 object property lookups를 캐싱하세요.
Incorrect (3 lookups × N iterations):
for (let i = 0; i < arr.length; i++) {
process(obj.config.settings.value);
}Correct (총 1 lookup):
const value = obj.config.settings.value;
const len = arr.length;
for (let i = 0; i < len; i++) {
process(value);
}| title | impact | impactDescription | tags |
|---|---|---|---|
| Storage API 호출 캐싱 | LOW-MEDIUM | 비용 큰 I/O 감소 | javascript, localStorage, storage, caching, performance |
Storage API 호출 캐싱
localStorage, sessionStorage, document.cookie는 동기적이고 비용이 큽니다. memory에 reads를 캐싱하세요.
Incorrect (caching 없이 반복 접근):
function getTheme() {
return localStorage.getItem("theme") ?? "light";
}
// 10번 호출 = 10번 storage readsCorrect (Map 기반 Cache):
const storageCache = new Map<string, string | null>();
function getLocalStorage(key: string) {
if (!storageCache.has(key)) {
storageCache.set(key, localStorage.getItem(key));
}
return storageCache.get(key);
}
function setLocalStorage(key: string, value: string) {
localStorage.setItem(key, value);
storageCache.set(key, value); // cache 동기화 유지
}components와 handlers 전체에서 utility-wide 가용성을 위해 hooks가 아닌 Map을 사용하세요.
Cookie Caching:
let cookieCache: Record<string, string> | null = null;
function getCookie(name: string) {
if (!cookieCache) {
cookieCache = Object.fromEntries(document.cookie.split("; ").map(c => c.split("=")));
}
return cookieCache[name];
}Cache Invalidation:
// storage가 외부에서 변경되면 cache 초기화
window.addEventListener("storage", e => {
if (e.key) storageCache.delete(e.key);
});
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
storageCache.clear();
}
});| title | impact | impactDescription | tags |
|---|---|---|---|
| 여러 배열 Iterations 결합 | LOW-MEDIUM | iterations 감소 | javascript, arrays, loops, performance |
여러 배열 Iterations 결합
동일한 배열에서 여러 .filter() 또는 .map() methods를 호출하면 불필요한 성능 overhead가 발생합니다. 각 작업에 대해 배열을 별도로 순회하는 대신, logic을 단일 loop로 통합하는 것이 더 효율적입니다.
Incorrect (3번 iterations):
const active = users.filter(u => u.isActive);
const admins = users.filter(u => u.role === "admin");
const verified = users.filter(u => u.isVerified);Correct (1번 iteration):
const active: User[] = [];
const admins: User[] = [];
const verified: User[] = [];
for (const user of users) {
if (user.isActive) active.push(user);
if (user.role === "admin") admins.push(user);
if (user.isVerified) verified.push(user);
}이 최적화는 여러 passes의 누적 비용이 더 눈에 띄는 대규모 datasets 작업 시 특히 유용합니다.
| title | impact | impactDescription | tags |
|---|---|---|---|
| 함수에서 Early Return | LOW-MEDIUM | 불필요한 계산 방지 | javascript, functions, optimization, early-return |
함수에서 Early Return
결과가 결정되면 불필요한 처리를 건너뛰기 위해 early return하세요.
Incorrect (답을 찾은 후에도 모든 items 처리):
function validateUsers(users: User[]) {
let hasError = false;
let errorMessage = "";
for (const user of users) {
if (!user.email) {
hasError = true;
errorMessage = "Email required";
}
if (!user.name) {
hasError = true;
errorMessage = "Name required";
}
// error가 발견된 후에도 모든 users 계속 확인
}
return hasError ? { valid: false, error: errorMessage } : { valid: true };
}Correct (첫 error에서 즉시 return):
function validateUsers(users: User[]) {
for (const user of users) {
if (!user.email) {
return { valid: false, error: "Email required" };
}
if (!user.name) {
return { valid: false, error: "Name required" };
}
}
return { valid: true };
}| title | impact | impactDescription | tags |
|---|---|---|---|
| flatMap으로 Map과 Filter를 한 번에 | LOW-MEDIUM | 중간 배열 제거 | javascript, arrays, flatmap, performance |
flatMap으로 Map과 Filter를 한 번에
.map().filter(Boolean)을 chaining하면 중간 배열이 생성되고 두 번 순회합니다. .flatMap()을 사용하여 한 번의 pass로 transform과 filter를 수행하세요.
Incorrect (2 iterations, 중간 배열):
const userNames = users.map(user => (user.isActive ? user.name : null)).filter(Boolean);Correct (1 iteration, 중간 배열 없음):
const userNames = users.flatMap(user => (user.isActive ? [user.name] : []));더 많은 예시:
// responses에서 valid emails 추출
// Before
const emails = responses.map(r => (r.success ? r.data.email : null)).filter(Boolean);
// After
const emails = responses.flatMap(r => (r.success ? [r.data.email] : []));
// valid numbers parse 및 filter
// Before
const numbers = strings.map(s => parseInt(s, 10)).filter(n => !isNaN(n));
// After
const numbers = strings.flatMap(s => {
const n = parseInt(s, 10);
return isNaN(n) ? [] : [n];
});사용 시점:
- items를 transform하면서 일부를 filter out
- 일부 inputs가 output을 생성하지 않는 conditional mapping
- invalid inputs를 건너뛰어야 하는 parsing/validating
| title | impact | impactDescription | tags |
|---|---|---|---|
| RegExp 생성 끌어올리기 | LOW-MEDIUM | 재생성 방지 | javascript, regexp, optimization, memoization |
RegExp 생성 끌어올리기
render 안에서 RegExp를 생성하지 마세요. module scope로 끌어올리거나 useMemo()로 memoize하세요.
Incorrect (매 render마다 new RegExp):
function Highlighter({ text, query }: Props) {
const regex = new RegExp(`(${query})`, "gi");
const parts = text.split(regex);
return <>{parts.map((part, i) => ...)}</>;
}Correct (memoize 또는 hoist):
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function Highlighter({ text, query }: Props) {
const regex = useMemo(
() => new RegExp(`(${escapeRegex(query)})`, "gi"),
[query]
);
const parts = text.split(regex);
return <>{parts.map((part, i) => ...)}</>;
}Warning (global regex는 mutable state를 가짐):
Global regex (/g)는 mutable lastIndex state를 가집니다:
const regex = /foo/g;
regex.test("foo"); // true, lastIndex = 3
regex.test("foo"); // false, lastIndex = 0| title | impact | impactDescription | tags |
|---|---|---|---|
| 반복 Lookups를 위한 Index Maps 구축 | MEDIUM-HIGH | O(n)에서 O(1)로 | javascript, map, lookup, performance |
반복 Lookups를 위한 Index Maps 구축
이 최적화 기법은 흔한 성능 문제를 해결합니다: 동일한 key로 같은 배열에서 .find()를 반복 호출하는 것.
문제점:
배열에서 .find()를 여러 번 호출하면, 각 호출이 전체 배열을 스캔해야 합니다—O(n) 연산. 여러 items에 대해 이렇게 하면 복잡도가 곱해집니다.
해결책:
배열에서 한 번 Map을 생성하고, lookup key를 map key로 사용하세요. 한 번 Map 구축 (O(n)), 이후 모든 lookups는 O(1).
Incorrect (N × M 연산):
const orders = getOrders(); // 1000개 orders
const users = getUsers(); // 1000명 users
const enriched = orders.map(order => ({
...order,
user: users.find(u => u.id === order.userId), // 매번 1000명 스캔
}));
// 1000 orders × 1000 users = 1,000,000 연산Correct (N + M 연산):
const orders = getOrders();
const users = getUsers();
// 한 번의 iteration으로 Map 구축
const userMap = new Map(users.map(u => [u.id, u]));
const enriched = orders.map(order => ({
...order,
user: userMap.get(order.userId), // O(1) lookup
}));
// 1000 + 1000 = 2,000 연산효과: 1000 orders × 1000 users의 경우: 1M ops → 2K ops
| title | impact | impactDescription | tags |
|---|---|---|---|
| 배열 비교에서 Length 먼저 확인 | MEDIUM-HIGH | 불필요한 비용 큰 연산 방지 | javascript, arrays, performance, optimization, comparison |
배열 비교에서 Length 먼저 확인
sorting이나 deep equality checks 같은 비용 큰 연산을 포함하는 배열 비교 시, 먼저 length 비교를 수행하면 상당한 성능 이점을 제공합니다. 다른 lengths의 배열은 같을 수 없으므로, 이것이 이상적인 early exit point입니다.
Incorrect (항상 sort 및 join, lengths가 달라도):
function arraysEqual(a: string[], b: string[]): boolean {
// 항상 두 개의 O(n log n) sorts + join 연산
return a.toSorted().join(",") === b.toSorted().join(",");
}Correct (먼저 O(1) length 확인):
function arraysEqual(a: string[], b: string[]): boolean {
if (a.length !== b.length) return false;
const sortedA = a.toSorted();
const sortedB = b.toSorted();
for (let i = 0; i < sortedA.length; i++) {
if (sortedA[i] !== sortedB[i]) return false;
}
return true;
}주요 Benefits:
- 배열 lengths가 일치하지 않으면 sorting과 joining 방지
- 대규모 배열의 string concatenation으로 인한 메모리 소비 감소
- mutation으로부터 원본 배열 보존
- 차이점 발견 시 early return
이 기법은 배열 비교가 빈번하게 실행되는 event handlers나 rendering loops 같은 성능-critical code paths에서 특히 유용합니다.
| title | impact | impactDescription | tags |
|---|---|---|---|
| Min/Max에 Sort 대신 Loop 사용 | LOW | O(n log n) 대신 O(n) | javascript, arrays, performance, sorting, algorithms |
Min/Max에 Sort 대신 Loop 사용
최소 또는 최대 element를 찾는 것은 배열을 한 번만 통과하면 됩니다. 극단값 하나 또는 두 개를 추출하기 위해 전체 collection을 sorting하면 불필요한 O(n log n) 연산으로 계산 리소스가 낭비됩니다.
Incorrect (sort로 min/max 찾기):
function getMaxPrice(items: Item[]): number | null {
if (items.length === 0) return null;
const sorted = items.toSorted((a, b) => b.price - a.price);
return sorted[0].price;
}Correct (한 번의 loop):
function getMaxPrice(items: Item[]): number | null {
if (items.length === 0) return null;
let max = items[0].price;
for (let i = 1; i < items.length; i++) {
if (items[i].price > max) {
max = items[i].price;
}
}
return max;
}min과 max 동시에 찾기:
function getMinMax(items: Item[]): { min: number; max: number } | null {
if (items.length === 0) return null;
let min = items[0].price;
let max = items[0].price;
for (let i = 1; i < items.length; i++) {
const price = items[i].price;
if (price < min) min = price;
if (price > max) max = price;
}
return { min, max };
}대안 고려사항: Math.min()과 Math.max()는 spread syntax로 작은 배열에 작동하지만 실질적인 한계가 있습니다—Chrome 143에서 약 124,000개 elements, Safari 18에서 638,000개—loop 접근법이 대규모 datasets에 더 안정적입니다.
| title | impact | impactDescription | tags |
|---|---|---|---|
| O(1) Lookups에 Set/Map 사용 | LOW-MEDIUM | O(n)에서 O(1)로 | javascript, set, map, data-structures, performance |
O(1) Lookups에 Set/Map 사용
반복적인 membership checks를 위해 배열을 Set/Map으로 변환하세요.
Incorrect (확인당 O(n)):
const allowedIds = ["a", "b", "c" /* ... */];
items.filter(item => allowedIds.includes(item.id));Correct (확인당 O(1)):
const allowedIds = new Set(["a", "b", "c" /* ... */]);
items.filter(item => allowedIds.has(item.id));| title | impact | impactDescription | tags |
|---|---|---|---|
| 불변성을 위해 sort() 대신 toSorted() 사용 | MEDIUM-HIGH | React state에서 mutation 버그 방지 | javascript, arrays, immutability, react, state, mutation |
불변성을 위해 sort() 대신 toSorted() 사용
.sort()는 배열을 in place로 mutate하여 React state와 props에서 버그를 유발할 수 있습니다. mutation 없이 새 sorted 배열을 생성하는 .toSorted()를 사용하세요.
Incorrect (원본 배열 mutate):
function UserList({ users }: { users: User[] }) {
// users prop 배열을 mutate!
const sorted = useMemo(() => users.sort((a, b) => a.name.localeCompare(b.name)), [users]);
return <div>{sorted.map(renderUser)}</div>;
}Correct (새 배열 생성):
function UserList({ users }: { users: User[] }) {
// 새 sorted 배열 생성, 원본 unchanged
const sorted = useMemo(() => users.toSorted((a, b) => a.name.localeCompare(b.name)), [users]);
return <div>{sorted.map(renderUser)}</div>;
}React에서 중요한 이유:
- Props/state mutations가 React의 immutability model을 깨뜨림 - React는 props와 state가 read-only로 취급되기를 기대합니다
- stale closure 버그 유발 - closures (callbacks, effects) 안에서 배열을 mutating하면 예상치 못한 동작 발생
Browser support (older browsers용 fallback):
.toSorted()는 모든 modern browsers에서 사용 가능합니다 (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). older 환경에서는 spread operator 사용:
// older browsers용 fallback
const sorted = [...items].sort((a, b) => a.value - b.value);기타 immutable 배열 methods:
.toSorted()- immutable sort.toReversed()- immutable reverse.toSpliced()- immutable splice.with()- immutable element replacement