Incremental audio production with duration mismatch handling, adaptive stem extension, and pre-mix alignment verification
日本語の概要は準備中です。原文の説明を表示しています。
Implement efficient virtual scrolling for rendering large lists with DOM recycling, chunk-based rendering, and performance optimizations
インストール方法を見るインストールする前に、エージェントに与えられる指示の中身を確認できます。
This skill teaches you how to implement a high-performance virtual scrolling list component that can efficiently render thousands or millions of items by only creating DOM nodes for visible items plus a small buffer.
The VirtualList component from /worldmonitor/src/components/VirtualList.ts demonstrates professional-grade virtual scrolling with the following patterns:
The implementation uses two complementary strategies:
PooledElement[])scrollTop, itemHeight, and viewport dimensionsoverscan parameter) to prevent flickering during scrollprivate handleScroll = (): void => {
if (this.scrollRAF !== null) return;
this.scrollRAF = requestAnimationFrame(() => {
this.scrollRAF = null;
if (!this.isDestroyed) {
this.updateVisibleRange();
}
});
};
Key Optimizations:
{ passive: true } improves scroll performanceThe implementation uses a sophisticated two-pass recycling algorithm:
Pass 1: Identify Reusable Elements
for (const pooled of this.itemPool) {
if (pooled.currentIndex >= visibleStart && pooled.currentIndex < visibleEnd) {
usedIndices.add(pooled.currentIndex);
}
}
Pass 2: Recycle and Reassign
while (poolIndex < this.itemPool.length) {
const pooled = this.itemPool[poolIndex]!;
if (pooled.currentIndex < visibleStart || pooled.currentIndex >= visibleEnd) {
// Recycle this element
if (this.onRecycle) {
this.onRecycle(pooled.element);
}
pooled.currentIndex = i;
this.renderItem(i, pooled.element);
pooled.element.style.transform = `translateY(${i * this.itemHeight}px)`;
poolIndex++;
break;
}
poolIndex++;
}
Benefits:
onRecycle callbacktransform: translateY() for GPU-accelerated positioningthis.topSpacer.style.height = `${visibleStart * this.itemHeight}px`;
this.bottomSpacer.style.height = `${Math.max(0, (this.totalItems - visibleEnd) * this.itemHeight)}px`;
if (visibleStart === this.visibleStart && visibleEnd === this.visibleEnd) {
return;
}
element.style.position = 'absolute';
element.style.top = '0';
element.style.left = '0';
element.style.right = '0';
element.style.transform = 'translateY(-9999px)'; // Hide off-screen
if (typeof ResizeObserver !== 'undefined') {
this.resizeObserver = new ResizeObserver(() => {
if (!this.isDestroyed) {
this.updateVisibleRange();
}
});
this.resizeObserver.observe(this.viewport);
}
Below is a simplified but fully functional virtual list implementation that you can drop into any vanilla TypeScript project:
/**
* SimpleVirtualList - A simplified virtual scrolling implementation
*
* Usage:
* ```typescript
* const virtualList = new SimpleVirtualList({
* container: document.getElementById('my-container')!,
* itemHeight: 50,
* totalItems: 100000,
* renderItem: (index, element) => {
* element.textContent = `Item ${index}`;
* }
* });
* ```
*/
export interface SimpleVirtualListOptions {
/** Container element to render the virtual list into */
container: HTMLElement;
/** Fixed height of each item in pixels */
itemHeight: number;
/** Total number of items in the list */
totalItems: number;
/** Callback to render content for an item */
renderItem: (index: number, element: HTMLElement) => void;
/** Number of items to render beyond visible area (default: 5) */
overscan?: number;
/** Optional callback when item element is recycled */
onRecycle?: (element: HTMLElement) => void;
}
interface VirtualItem {
element: HTMLElement;
index: number;
}
export class SimpleVirtualList {
// Configuration
private container: HTMLElement;
private itemHeight: number;
private totalItems: number;
private overscan: number;
private renderItem: (index: number, element: HTMLElement) => void;
private onRecycle?: (element: HTMLElement) => void;
// DOM Structure
private viewport: HTMLElement;
private contentWrapper: HTMLElement;
private topSpacer: HTMLElement;
private bottomSpacer: HTMLElement;
// State
private itemPool: VirtualItem[] = [];
private visibleStart = 0;
private visibleEnd = 0;
private rafId: number | null = null;
private destroyed = false;
constructor(options: SimpleVirtualListOptions) {
this.container = options.container;
this.itemHeight = options.itemHeight;
this.totalItems = options.totalItems;
this.overscan = options.overscan ?? 5;
this.renderItem = options.renderItem;
this.onRecycle = options.onRecycle;
this.setupDOM();
this.attachEventListeners();
this.updateVisibleRange();
}
/**
* Set up the DOM structure for virtual scrolling
*/
private setupDOM(): void {
// Clear container
this.container.innerHTML = '';
// Create viewport (scrollable container)
this.viewport = document.createElement('div');
this.viewport.style.cssText = `
width: 100%;
height: 100%;
overflow-y: auto;
position: relative;
`;
// Create content wrapper
this.contentWrapper = document.createElement('div');
const totalHeight = this.totalItems * this.itemHeight;
this.contentWrapper.style.cssText = `
position: relative;
width: 100%;
height: ${totalHeight}px;
`;
// Create spacers
this.topSpacer = document.createElement('div');
this.topSpacer.style.cssText = `
height: 0px;
width: 100%;
`;
this.bottomSpacer = document.createElement('div');
this.bottomSpacer.style.cssText = `
height: ${totalHeight}px;
width: 100%;
`;
// Assemble DOM
this.contentWrapper.appendChild(this.topSpacer);
this.contentWrapper.appendChild(this.bottomSpacer);
this.viewport.appendChild(this.contentWrapper);
this.container.appendChild(this.viewport);
}
/**
* Attach scroll event listeners with optimization
*/
private attachEventListeners(): void {
this.viewport.addEventListener('scroll', this.handleScroll, { passive: true });
}
/**
* Throttled scroll handler using requestAnimationFrame
*/
private handleScroll = (): void => {
// Prevent multiple RAF calls per frame
if (this.rafId !== null) return;
this.rafId = requestAnimationFrame(() => {
this.rafId = null;
if (!this.destroyed) {
this.updateVisibleRange();
}
});
};
/**
* Calculate and update which items should be visible
*/
private updateVisibleRange(): void {
const scrollTop = this.viewport.scrollTop;
const viewportHeight = this.viewport.clientHeight;
// Calculate visible indices
const startIndex = Math.floor(scrollTop / this.itemHeight);
const endIndex = Math.ceil((scrollTop + viewportHeight) / this.itemHeight);
// Add overscan buffer
const bufferedStart = Math.max(0, startIndex - this.overscan);
const bufferedEnd = Math.min(this.totalItems, endIndex + this.overscan);
// Skip if range hasn't changed
if (bufferedStart === this.visibleStart && bufferedEnd === this.visibleEnd) {
return;
}
this.visibleStart = bufferedStart;
this.visibleEnd = bufferedEnd;
// Update spacers to maintain scroll position
this.topSpacer.style.height = `${bufferedStart * this.itemHeight}px`;
const bottomHeight = Math.max(0, (this.totalItems - bufferedEnd) * this.itemHeight);
this.bottomSpacer.style.height = `${bottomHeight}px`;
// Render visible items
this.renderVisibleItems();
}
/**
* Render or update items in the visible range using DOM recycling
*/
private renderVisibleItems(): void {
const visibleCount = this.visibleEnd - this.visibleStart;
// Ensure we have enough pooled elements
this.ensurePoolSize(visibleCount);
// Track which indices are currently in use
const activeIndices = new Set<number>();
// First pass: keep elements that are still visible
for (const item of this.itemPool) {
if (item.index >= this.visibleStart && item.index < this.visibleEnd) {
activeIndices.add(item.index);
}
}
// Second pass: recycle and assign new indices
let poolIndex = 0;
for (let i = this.visibleStart; i < this.visibleEnd; i++) {
// Skip if already rendered
if (activeIndices.has(i)) continue;
// Find an element to recycle
while (poolIndex < this.itemPool.length) {
const item = this.itemPool[poolIndex];
// Check if this element can be recycled
if (item.index < this.visibleStart || item.index >= this.visibleEnd) {
// Call recycle callback
if (this.onRecycle) {
this.onRecycle(item.element);
}
// Update item
item.index = i;
this.renderItem(i, item.element);
this.positionItem(item);
poolIndex++;
break;
}
poolIndex++;
}
}
// Third pass: update positions and visibility
for (const item of this.itemPool) {
if (item.index >= this.visibleStart && item.index < this.visibleEnd) {
// Ensure proper position
this.positionItem(item);
item.element.style.visibility = 'visible';
} else {
// Hide off-screen items
item.element.style.visibility = 'hidden';
item.element.style.transform = 'translateY(-9999px)';
}
}
}
/**
* Position an item element using transform for GPU acceleration
*/
private positionItem(item: VirtualItem): void {
const yOffset = item.index * this.itemHeight;
item.element.style.transform = `translateY(${yOffset}px)`;
}
/**
* Ensure the pool has enough elements
*/
private ensurePoolSize(requiredSize: number): void {
while (this.itemPool.length < requiredSize) {
const element = this.createItemElement();
const item: VirtualItem = {
element,
index: -1,
};
this.itemPool.push(item);
// Insert before bottom spacer
this.contentWrapper.insertBefore(element, this.bottomSpacer);
}
}
/**
* Create a new item element with proper styling
*/
private createItemElement(): HTMLElement {
const element = document.createElement('div');
element.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: ${this.itemHeight}px;
box-sizing: border-box;
visibility: hidden;
transform: translateY(-9999px);
`;
element.classList.add('virtual-list-item');
return element;
}
/**
* Update the total number of items
*/
setTotalItems(count: number): void {
this.totalItems = count;
// Update content height
const totalHeight = this.totalItems * this.itemHeight;
this.contentWrapper.style.height = `${totalHeight}px`;
// Recalculate visible range
this.updateVisibleRange();
}
/**
* Scroll to a specific item index
*/
scrollToIndex(index: number, smooth = false): void {
const offset = Math.max(0, Math.min(index, this.totalItems - 1)) * this.itemHeight;
this.viewport.scrollTo({
top: offset,
behavior: smooth ? 'smooth' : 'auto',
});
}
/**
* Force refresh all visible items
*/
refresh(): void {
// Mark all items as needing re-render
for (const item of this.itemPool) {
item.index = -1;
}
// Trigger update
this.updateVisibleRange();
}
/**
* Get the current scroll position info
*/
getScrollInfo(): { scrollTop: number; visibleStart: number; visibleEnd: number } {
return {
scrollTop: this.viewport.scrollTop,
visibleStart: this.visibleStart,
visibleEnd: this.visibleEnd,
};
}
/**
* Clean up resources and remove event listeners
*/
destroy(): void {
this.destroyed = true;
// Cancel pending animation frame
if (this.rafId !== null) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
// Remove event listeners
this.viewport.removeEventListener('scroll', this.handleScroll);
// Clear pool
this.itemPool = [];
// Clear container
this.container.innerHTML = '';
}
}
// ============================================================================
// CSS Styles (add to your stylesheet or inject programmatically)
// ============================================================================
/**
* Recommended CSS for virtual list container:
*
* ```css
* .virtual-list-container {
* width: 100%;
* height: 400px; // or whatever height you need
* border: 1px solid #ddd;
* overflow: hidden;
* }
*
* .virtual-list-item {
* border-bottom: 1px solid #eee;
* padding: 10px;
* display: flex;
* align-items: center;
* }
*
* .virtual-list-item:hover {
* background-color: #f5f5f5;
* }
* ```
*/
// ============================================================================
// Usage Example
// ============================================================================
/**
* Example 1: Basic Usage
*/
export function exampleBasicUsage(): void {
const container = document.createElement('div');
container.className = 'virtual-list-container';
container.style.cssText = 'width: 500px; height: 400px; border: 1px solid #ddd;';
document.body.appendChild(container);
const virtualList = new SimpleVirtualList({
container,
itemHeight: 50,
totalItems: 100000,
renderItem: (index, element) => {
element.innerHTML = `
<div style="padding: 10px; border-bottom: 1px solid #eee;">
<strong>Item ${index}</strong>
<span style="margin-left: 20px; color: #666;">
${new Date(Date.now() - index * 1000 * 60).toLocaleString()}
</span>
</div>
`;
},
});
// Scroll to item 5000 after 2 seconds
setTimeout(() => {
virtualList.scrollToIndex(5000, true);
}, 2000);
}
/**
* Example 2: With Cleanup and Event Listeners
*/
export function exampleWithCleanup(): void {
const container = document.getElementById('list-container') as HTMLElement;
const virtualList = new SimpleVirtualList({
container,
itemHeight: 60,
totalItems: 50000,
renderItem: (index, element) => {
element.innerHTML = `
<div class="item-content">
<h4>User ${index}</h4>
<button class="delete-btn" data-index="${index}">Delete</button>
</div>
`;
// Add event listener
const btn = element.querySelector('.delete-btn') as HTMLElement;
if (btn) {
btn.addEventListener('click', () => {
console.log(`Delete item ${index}`);
});
}
},
onRecycle: (element) => {
// Clean up event listeners when element is recycled
const btn = element.querySelector('.delete-btn') as HTMLElement;
if (btn) {
// Clone and replace to remove all listeners
const newBtn = btn.cloneNode(true);
btn.parentNode?.replaceChild(newBtn, btn);
}
},
});
// Example: Update total items dynamically
setTimeout(() => {
virtualList.setTotalItems(100000);
}, 5000);
}
/**
* Example 3: Integration with Data Source
*/
export class DataVirtualList<T> {
private virtualList: SimpleVirtualList;
private data: T[] = [];
private renderer: (item: T, index: number, element: HTMLElement) => void;
constructor(
container: HTMLElement,
itemHeight: number,
renderer: (item: T, index: number, element: HTMLElement) => void
) {
this.renderer = renderer;
this.virtualList = new SimpleVirtualList({
container,
itemHeight,
totalItems: 0,
renderItem: (index, element) => {
if (index < this.data.length) {
this.renderer(this.data[index], index, element);
}
},
});
}
setData(data: T[]): void {
this.data = data;
this.virtualList.setTotalItems(data.length);
}
refresh(): void {
this.virtualList.refresh();
}
scrollToIndex(index: number): void {
this.virtualList.scrollToIndex(index, true);
}
destroy(): void {
this.virtualList.destroy();
}
}
// Example usage with typed data
interface User {
id: number;
name: string;
email: string;
}
export function exampleTypedData(): void {
const container = document.getElementById('user-list') as HTMLElement;
const userList = new DataVirtualList<User>(
container,
80,
(user, index, element) => {
element.innerHTML = `
<div style="padding: 15px; border-bottom: 1px solid #e0e0e0;">
<div style="font-weight: bold; font-size: 16px;">${user.name}</div>
<div style="color: #666; font-size: 14px;">${user.email}</div>
<div style="color: #999; font-size: 12px;">ID: ${user.id}</div>
</div>
`;
}
);
// Generate sample data
const users: User[] = Array.from({ length: 100000 }, (_, i) => ({
id: i,
name: `User ${i}`,
email: `user${i}@example.com`,
}));
userList.setData(users);
}
Container
└── Viewport (scrollable)
└── ContentWrapper (full height)
├── TopSpacer (variable height)
├── Item Elements (absolute positioned)
└── BottomSpacer (variable height)
Scroll Event → RAF Throttle → Calculate Visible Range → Update Spacers → Recycle Items → Render Content
requestAnimationFrame for scroll throttling{ passive: true } to scroll listenerstransform instead of top/left for positioningForgetting to clean up event listeners → Memory leaks
onRecycle callbackNot using passive listeners → Janky scrolling
{ passive: true }Synchronous expensive rendering → Dropped frames
renderItem fast, defer heavy workVariable item heights without measurement → Misaligned items
Not handling resize events → Broken layout
// Cache measured heights
private heightCache = new Map<number, number>();
private measureItem(index: number, element: HTMLElement): number {
if (!this.heightCache.has(index)) {
const height = element.getBoundingClientRect().height;
this.heightCache.set(index, height);
}
return this.heightCache.get(index)!;
}
// Track section headers
private sectionHeaders = new Map<number, string>();
private updateStickyHeader(): void {
const scrollTop = this.viewport.scrollTop;
const currentSection = this.getSectionAtOffset(scrollTop);
this.stickyHeaderElement.textContent = currentSection;
}
// Support horizontal + vertical scrolling
private updateVisibleRange2D(): void {
const scrollX = this.viewport.scrollLeft;
const scrollY = this.viewport.scrollTop;
const colStart = Math.floor(scrollX / this.itemWidth);
const rowStart = Math.floor(scrollY / this.itemHeight);
// ... calculate visible grid cells
}
// Test with various scenarios
describe('SimpleVirtualList', () => {
it('renders only visible items', () => {
const list = new SimpleVirtualList({...});
expect(list.getScrollInfo().visibleEnd - list.getScrollInfo().visibleStart)
.toBeLessThan(50); // Even with 100k items
});
it('recycles elements on scroll', () => {
const recycledIndices: number[] = [];
const list = new SimpleVirtualList({
onRecycle: (el) => recycledIndices.push(parseInt(el.dataset.index!)),
...
});
list.scrollToIndex(1000);
expect(recycledIndices.length).toBeGreaterThan(0);
});
it('handles rapid scrolling', async () => {
const list = new SimpleVirtualList({...});
for (let i = 0; i < 100; i++) {
list.scrollToIndex(i * 100);
await nextFrame();
}
// Should not crash or have visual glitches
});
});
Virtual scrolling is a critical technique for rendering large datasets efficiently. The key principles are:
This implementation can handle millions of items with smooth 60fps scrolling and minimal memory usage.
まだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
Incremental audio production with duration mismatch handling, adaptive stem extension, and pre-mix alignment verification
日本語の概要は準備中です。原文の説明を表示しています。
Incremental audio production with duration alignment handling, per-stem verification, and adaptive extension strategies
日本語の概要は準備中です。原文の説明を表示しています。
Create serverless API proxy endpoints that hide API keys and provide a unified backend for the dashboard frontend. Designed for Vercel deployment.
日本語の概要は準備中です。原文の説明を表示しています。
End-to-end audio production workflow with stems, effects, archiving, and verification
日本語の概要は準備中です。原文の説明を表示しています。
Handle cascading data retrieval tool failures by falling back to embedded knowledge generation
日本語の概要は準備中です。原文の説明を表示しています。
Fallback pattern for executing Python code when execute_code_sandbox fails
日本語の概要は準備中です。原文の説明を表示しています。