perf(B06): 카드 넘침 정리를 한 프레임에 몰아 강제 레이아웃 67회를 1회로
카드마다 requestAnimationFrame(reflow) 을 걸어 한 프레임에 67번 돌았고, 매번 쓰기 뒤 읽기라 강제 레이아웃이 67회 났음. 한 번의 강제 레이아웃이 그때까지 들어간 카드 전부를 다시 재므로 뒤로 갈수록 비쌌음(보조 창 CPU 프로파일: 자기 시간 1위). - reflow 를 reset(쓰기) / measure(읽기) / apply(쓰기) 세 토막으로 가름. - 모듈 단위 scheduleReflow 가 카드를 모아 한 프레임에서 전부 reset -> 전부 measure -> 전부 apply 순으로 돌림. 강제 레이아웃 프레임당 1회. - ResizeObserver 는 창 크기 변경 때 그 카드만 큐에 다시 넣음(첫 호출 건너뛰기 유지). 자체검증(공용 브라우저) — B06 진입 2,330~2,864ms -> 1,994/2,088ms, 긴 작업 합 1,853 -> 1,604/1,728ms. 동작 확인: 카드를 200px 로 좁히면 버튼 3개가 ... 패널로 옮겨지고 되돌리면 복귀(정상 폭에서는 scrollWidth = clientWidth = 937 로 넘침 없음). typecheck 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,36 @@ export {
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
/** 넘침 정리 한 장 몫 — 쓰기·읽기·쓰기 세 토막으로 갈라 두어 프레임 단위로 묶는다. */
|
||||
interface ReflowCard {
|
||||
reset(): void;
|
||||
/** 패널로 옮길 개수. -1 이면 아직 자리를 안 잡아 건드리지 않는다. */
|
||||
measure(): number;
|
||||
apply(moveCount: number): void;
|
||||
}
|
||||
|
||||
const reflowQueue = new Set<ReflowCard>();
|
||||
let reflowScheduled = false;
|
||||
|
||||
/**
|
||||
* 넘침 정리를 **한 프레임에 몰아** 돌린다 — 모든 카드의 쓰기를 먼저 끝내고, 그 다음
|
||||
* 읽기를 몰아서 하고, 마지막에 쓰기를 몰아서 한다. 강제 레이아웃이 카드 수만큼(67회)
|
||||
* 나던 것이 프레임당 한 번으로 줄어든다.
|
||||
*/
|
||||
function scheduleReflow(card: ReflowCard): void {
|
||||
reflowQueue.add(card);
|
||||
if (reflowScheduled) return;
|
||||
reflowScheduled = true;
|
||||
requestAnimationFrame(() => {
|
||||
reflowScheduled = false;
|
||||
const cards = [...reflowQueue];
|
||||
reflowQueue.clear();
|
||||
for (const item of cards) item.reset();
|
||||
const counts = cards.map((item) => item.measure());
|
||||
cards.forEach((item, index) => item.apply(counts[index]));
|
||||
});
|
||||
}
|
||||
|
||||
/** 카드 버튼줄의 flex 간격(px) — 모든 카드가 같은 CSS 를 쓰므로 한 번만 잰다.
|
||||
* `getComputedStyle` 도 강제 레이아웃을 부르므로 카드 67장마다 부르지 않는다. */
|
||||
let barGapPx: number | null = null;
|
||||
@@ -416,50 +446,52 @@ export function buildDesignControls(
|
||||
more.append(moreSummary, morePanel);
|
||||
bar.append(slopeDirSeg, ...moveable, more);
|
||||
|
||||
const reflow = (): void => {
|
||||
// 후보 전부 인라인 복귀 → 폭을 **한 번만 재고** → 넘치는 만큼 뒤에서부터 패널로 이동.
|
||||
//
|
||||
// 예전에는 한 칸 옮길 때마다 `bar.scrollWidth` 를 다시 읽어(쓰기→읽기→쓰기) 브라우저가
|
||||
// 매번 레이아웃을 강제로 다시 계산했다. 카드 67장마다 도는 자리라 B06 진입에서 일한
|
||||
// 시간의 19.4% 를 이 함수가 썼다(2026-09-06 CPU 프로파일). 읽기와 쓰기를 갈랐다.
|
||||
// 넘침 정리는 **세 토막**으로 나눈다 — 쓰기(reset) → 읽기(measure) → 쓰기(apply).
|
||||
// 카드 안에서 한 장씩 하면 쓰기 뒤 읽기가 카드 수만큼 반복돼 브라우저가 레이아웃을
|
||||
// 그때마다 강제로 다시 잰다. 게다가 한 번의 강제 레이아웃이 **그때까지 들어간 카드
|
||||
// 전부**를 다시 재므로 뒤로 갈수록 비싸진다(2026-09-06 CPU 프로파일: 진입에서 자기
|
||||
// 시간 1위). 그래서 `scheduleReflow` 가 67장을 모아 한 프레임에 묶어 돌린다.
|
||||
const card: ReflowCard = {
|
||||
reset() {
|
||||
for (const element of moveable) bar.insertBefore(element, more);
|
||||
morePanel.replaceChildren();
|
||||
more.hidden = false; // 폭을 재려면 자리에 있어야 한다.
|
||||
|
||||
},
|
||||
measure() {
|
||||
const clientWidth = bar.clientWidth;
|
||||
if (clientWidth <= 0) {
|
||||
more.hidden = true;
|
||||
return;
|
||||
}
|
||||
if (clientWidth <= 0) return -1; // 아직 자리를 안 잡았다 — 그대로 둔다.
|
||||
if (barGapPx === null) barGapPx = Number.parseFloat(getComputedStyle(bar).gap) || 0;
|
||||
const widths = moveable.map((element) => element.offsetWidth);
|
||||
let overflow = bar.scrollWidth - clientWidth;
|
||||
|
||||
let moveCount = 0;
|
||||
while (overflow > 1 && moveCount < moveable.length) {
|
||||
overflow -= widths[moveable.length - 1 - moveCount] + barGapPx;
|
||||
moveCount += 1;
|
||||
}
|
||||
if (moveCount === 0) {
|
||||
return moveCount;
|
||||
},
|
||||
apply(moveCount) {
|
||||
if (moveCount <= 0) {
|
||||
more.hidden = true;
|
||||
return;
|
||||
}
|
||||
for (let index = 0; index < moveCount; index += 1) {
|
||||
morePanel.insertBefore(moveable[moveable.length - 1 - index], morePanel.firstChild);
|
||||
}
|
||||
},
|
||||
};
|
||||
// 첫 호출은 아래 `requestAnimationFrame` 이 맡는다 — 관찰을 걸면 초기 크기로 곧바로 한 번
|
||||
// 더 불려 카드마다 reflow 가 두 번 돌았다.
|
||||
// 첫 호출은 아래 `scheduleReflow` 가 맡는다 — 관찰을 걸면 초기 크기로 곧바로 한 번 더
|
||||
// 불려 카드마다 두 번 돌았다. 창 크기가 바뀔 때만 그 카드 하나를 다시 넣는다.
|
||||
let firstObservation = true;
|
||||
const overflowObserver = new ResizeObserver(() => {
|
||||
if (firstObservation) {
|
||||
firstObservation = false;
|
||||
return;
|
||||
}
|
||||
reflow();
|
||||
scheduleReflow(card);
|
||||
});
|
||||
overflowObserver.observe(bar);
|
||||
requestAnimationFrame(reflow);
|
||||
scheduleReflow(card);
|
||||
|
||||
// 암 경계선 제어는 그래프 X축 제목 행으로 이동(E-7, Cross_View에서 배치).
|
||||
// 절·성토 면적 readout은 그래프 중상단 오버레이로 이동(E-4, Cross_View에서 배치).
|
||||
|
||||
Reference in New Issue
Block a user