개요
성능 최적화는 대용량 문서를 처리하는 에디터에 중요합니다. 주요 전략에는 증분 업데이트, 배칭, 가상 DOM, 지연 렌더링이 포함됩니다.
증분 DOM 업데이트
Diff 알고리즘
실제로 변경된 DOM 부분만 업데이트합니다:
class IncrementalRenderer {
#nodeMap = new Map(); // nodeId -> DOM element
update(document, previousDocument) {
// Find differences
const diff = this.#diff(previousDocument, document);
// Apply only the changes
diff.forEach(change => {
switch (change.type) {
case 'insert':
this.#insertNode(change.node, change.path);
break;
case 'delete':
this.#deleteNode(change.path);
break;
case 'update':
this.#updateNode(change.path, change.attrs);
break;
case 'move':
this.#moveNode(change.path, change.newPath);
break;
}
});
}
#diff(oldDoc, newDoc) {
const changes = [];
// Compare node trees using longest common subsequence
const lcs = this.#longestCommonSubsequence(
oldDoc.children,
newDoc.children
);
let oldIndex = 0;
let newIndex = 0;
let lcsIndex = 0;
while (oldIndex < oldDoc.children.length || newIndex < newDoc.children.length) {
if (lcsIndex < lcs.length &&
oldDoc.children[oldIndex]?.id === lcs[lcsIndex].id) {
// Node unchanged, recurse into children
this.#compareNodes(
oldDoc.children[oldIndex],
newDoc.children[newIndex],
[oldIndex],
changes
);
oldIndex++;
newIndex++;
lcsIndex++;
} else if (oldIndex < oldDoc.children.length &&
(lcsIndex >= lcs.length ||
oldDoc.children[oldIndex].id !== lcs[lcsIndex].id)) {
// Node deleted
changes.push({
type: 'delete',
path: [oldIndex]
});
oldIndex++;
} else {
// Node inserted
changes.push({
type: 'insert',
node: newDoc.children[newIndex],
path: [newIndex]
});
newIndex++;
}
}
return changes;
}
}패치 적용
패치를 효율적으로 적용합니다:
class PatchApplier {
applyPatches(container, patches) {
// Sort patches by index (reverse order for deletions)
const sorted = patches.sort((a, b) => {
if (a.type === 'delete' && b.type === 'delete') {
return b.index - a.index; // Delete from end
}
return a.index - b.index;
});
sorted.forEach(patch => {
switch (patch.type) {
case 'insert':
const newNode = this.#renderNode(patch.node);
container.insertBefore(newNode, container.children[patch.index]);
break;
case 'delete':
container.removeChild(container.children[patch.index]);
break;
case 'update':
this.#updateNodeAttributes(
container.children[patch.index],
patch.attrs
);
break;
case 'move':
const node = container.children[patch.from];
container.insertBefore(node, container.children[patch.to]);
break;
}
});
}
}배치 업데이트
업데이트 배칭
불필요한 렌더를 피하기 위해 여러 작업을 배치합니다:
class Editor {
#pendingOperations = [];
#renderScheduled = false;
applyOperation(operation) {
// Add to pending operations
this.#pendingOperations.push(operation);
// Schedule render (debounced)
this.#scheduleRender();
}
#scheduleRender() {
if (this.#renderScheduled) return;
this.#renderScheduled = true;
// Use requestAnimationFrame for smooth updates
requestAnimationFrame(() => {
this.#flushOperations();
this.#renderScheduled = false;
});
}
#flushOperations() {
if (this.#pendingOperations.length === 0) return;
// Apply all pending operations
const operations = this.#pendingOperations;
this.#pendingOperations = [];
// Batch apply
this.#batchApply(operations);
// Single render for all operations
this.render();
}
// Manual batching API
batch(callback) {
const wasBatching = this.#isBatching;
this.#isBatching = true;
try {
callback();
} finally {
this.#isBatching = wasBatching;
if (!this.#isBatching) {
this.#flushOperations();
}
}
}
}
// Usage
editor.batch(() => {
editor.insertText('Hello');
editor.insertText(' ');
editor.insertText('World');
// Single render after batch completes
});디바운싱
빈번한 업데이트를 디바운스합니다:
class DebouncedRenderer {
#pendingRender = null;
#delay = 16; // ~60fps
scheduleRender() {
if (this.#pendingRender) {
clearTimeout(this.#pendingRender);
}
this.#pendingRender = setTimeout(() => {
this.render();
this.#pendingRender = null;
}, this.#delay);
}
// Throttle for critical updates
throttleRender() {
if (this.#lastRender && Date.now() - this.#lastRender < this.#delay) {
return;
}
this.render();
this.#lastRender = Date.now();
}
}가상 DOM 패턴
VDOM 구현
DOM을 건드리기 전에 가상 표현을 사용하여 변경 사항을 계산합니다:
class VirtualDOM {
#root = null;
render(document) {
return this.#renderNode(document);
}
#renderNode(node) {
return {
type: node.type,
props: node.attrs || {},
key: node.id,
children: node.children?.map(child => this.#renderNode(child)) || []
};
}
patch(oldTree, newTree) {
// Compare virtual trees
const patches = this.#diff(oldTree, newTree);
// Apply patches to actual DOM
this.#applyPatches(patches);
}
#diff(oldNode, newNode) {
const patches = [];
if (oldNode.type !== newNode.type) {
// Node type changed, replace
patches.push({
type: 'replace',
oldNode,
newNode
});
} else {
// Same type, check props and children
const propPatches = this.#diffProps(oldNode.props, newNode.props);
if (propPatches.length > 0) {
patches.push({
type: 'props',
node: oldNode,
patches: propPatches
});
}
// Diff children
const childPatches = this.#diffChildren(oldNode.children, newNode.children);
patches.push(...childPatches);
}
return patches;
}
}재조정
효율적인 재조정 알고리즘:
class Reconciler {
reconcile(oldChildren, newChildren) {
// Use keys for efficient matching
const oldKeyMap = new Map(
oldChildren.map((child, i) => [child.key || i, i])
);
const newKeyMap = new Map(
newChildren.map((child, i) => [child.key || i, i])
);
const patches = [];
// Find moved nodes
for (const [key, newIndex] of newKeyMap) {
const oldIndex = oldKeyMap.get(key);
if (oldIndex !== undefined && oldIndex !== newIndex) {
patches.push({
type: 'move',
key,
from: oldIndex,
to: newIndex
});
}
}
// Find inserted nodes
for (const [key, newIndex] of newKeyMap) {
if (!oldKeyMap.has(key)) {
patches.push({
type: 'insert',
node: newChildren[newIndex],
index: newIndex
});
}
}
// Find deleted nodes
for (const [key, oldIndex] of oldKeyMap) {
if (!newKeyMap.has(key)) {
patches.push({
type: 'delete',
index: oldIndex
});
}
}
return patches;
}
}지연 렌더링
보이는 콘텐츠만 렌더링합니다:
class LazyRenderer {
#viewport = { top: 0, bottom: window.innerHeight };
#renderedNodes = new Set();
render(document) {
// Only render nodes in viewport
const visibleNodes = this.#getVisibleNodes(document);
visibleNodes.forEach(node => {
if (!this.#renderedNodes.has(node.id)) {
this.#renderNode(node);
this.#renderedNodes.add(node.id);
}
});
// Remove nodes outside viewport
this.#renderedNodes.forEach(nodeId => {
if (!visibleNodes.find(n => n.id === nodeId)) {
this.#removeNode(nodeId);
this.#renderedNodes.delete(nodeId);
}
});
}
#getVisibleNodes(document) {
// Calculate which nodes are in viewport
const nodes = [];
this.#traverse(document, (node, rect) => {
if (this.#isInViewport(rect)) {
nodes.push(node);
}
});
return nodes;
}
#isInViewport(rect) {
return rect.top < this.#viewport.bottom &&
rect.bottom > this.#viewport.top;
}
}