git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/perf-long-tasks" ~/.claude/skills/intense-visions-harness-engineering-perf-long-tasks && rm -rf "$T"
agents/skills/claude-code/perf-long-tasks/SKILL.mdLong Tasks
Detect, break up, and eliminate long tasks (>50ms on the main thread) using time-slicing, scheduler APIs, Web Workers, and cooperative yielding to keep the UI responsive and meet the 50ms responsiveness budget.
When to Use
- Chrome DevTools Performance panel shows red "Long Task" markers (tasks exceeding 50ms)
- INP is poor because user interactions are delayed by long-running tasks on the main thread
- Page initialization runs heavy synchronous work (data processing, DOM construction, parsing)
- You are processing large datasets (sorting, filtering, searching) synchronously on the main thread
- The Long Tasks API (
withPerformanceObserver
type) fires frequently in production monitoringlongtask - You need to choose between
,scheduler.yield()
,scheduler.postTask()
, and Web WorkersrequestIdleCallback
on multi-MB payloads blocks the main threadJSON.parse- Third-party scripts contribute long tasks that degrade responsiveness
- A computationally intensive feature (syntax highlighting, markdown parsing, image processing) needs to run without blocking
- Animation or scroll frame rate drops below 60fps due to JavaScript execution in the main thread
Instructions
-
Understand the 50ms budget. At 60fps, each frame is 16.67ms. The browser needs ~6ms for rendering (style, layout, paint, composite). That leaves ~10ms for JavaScript per frame. The Long Tasks API defines 50ms as the threshold because: a task under 50ms allows the browser to respond to input within 100ms (the perceptual threshold for "instant" response).
-
Detect long tasks with the Performance Observer:
const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log('Long task detected:', entry.duration, 'ms'); console.log('Attribution:', entry.attribution[0]?.containerName); } }); observer.observe({ type: 'longtask', buffered: true }); -
Break up long tasks with
. This is the preferred modern approach — it yields to the browser so pending input events can run, then resumes the work:scheduler.yield()async function processLargeDataset(items) { const CHUNK_SIZE = 100; for (let i = 0; i < items.length; i += CHUNK_SIZE) { const chunk = items.slice(i, i + CHUNK_SIZE); processChunk(chunk); // Yield to browser — allows input events and rendering await scheduler.yield(); } } -
Use
for priority-based scheduling:scheduler.postTask()// High priority — respond to user action await scheduler.postTask(() => updateSearchResults(query), { priority: 'user-blocking', }); // Low priority — prefetch next page data scheduler.postTask(() => prefetchNextPage(), { priority: 'background', }); -
Use
for truly non-urgent work:requestIdleCallbackfunction processAnalyticsQueue() { requestIdleCallback((deadline) => { // Process items while we have idle time (at least 5ms remaining) while (deadline.timeRemaining() > 5 && analyticsQueue.length > 0) { sendAnalyticsEvent(analyticsQueue.shift()); } // If more items remain, schedule another idle callback if (analyticsQueue.length > 0) { requestIdleCallback(processAnalyticsQueue); } }); } -
Move CPU-intensive work to Web Workers:
// main.js — offload to worker const worker = new Worker('/syntax-worker.js'); worker.postMessage({ text: editorContent }); worker.onmessage = (e) => { applyHighlighting(e.data.tokens); }; // syntax-worker.js — runs on a separate thread self.onmessage = (e) => { const tokens = parseSyntax(e.data.text); // 150ms of CPU work self.postMessage({ tokens }); // does not block main thread }; -
Use the MessageChannel trick for zero-delay yielding (pre-scheduler.yield):
function yieldToMain() { return new Promise((resolve) => { const channel = new MessageChannel(); channel.port1.onmessage = resolve; channel.port2.postMessage(null); }); } async function processItems(items) { for (const item of items) { processItem(item); await yieldToMain(); // no 4ms setTimeout clamp } }
Details
Why 50ms Is the Threshold
The 50ms threshold comes from the RAIL performance model:
- Response — respond to user input within 100ms
- The 100ms budget includes input processing (event handler) plus rendering
- If a task is already running when the user interacts, the interaction must wait for the task to finish
- A 50ms task + 50ms event processing + rendering = ~100ms total response time
Tasks under 50ms are safe because even if a user interaction arrives while the task is running, the worst-case delay is under 100ms. Tasks over 50ms risk perceptible delay.
Worked Example: Google Search Results Chunked Rendering
Google's search results page renders 10 results in 5 chunks of 2 results each, yielding between chunks with
scheduler.yield(). Without chunking, rendering all 10 results takes 80ms (a long task). With chunking:
- Chunk 1 (results 1-2): 16ms, yield
- Chunk 2 (results 3-4): 16ms, yield
- Chunk 3 (results 5-6): 16ms, yield
- Chunk 4 (results 7-8): 16ms, yield
- Chunk 5 (results 9-10): 16ms
Total time increases from 80ms to ~95ms (overhead from yielding), but no single task exceeds 50ms. INP improved from 200ms to 40ms because user interactions are no longer blocked by a monolithic render task.
Worked Example: Markdown Editor Web Worker
A markdown editor moved syntax highlighting to a Web Worker. The main thread sends raw text via
postMessage. The worker tokenizes the text (150ms of CPU work per keystroke on large documents) and returns highlighted tokens. The main thread applies the tokens to the DOM.
Before: typing in a 10,000-line document caused 150ms input lag per keystroke (long task blocking every keypress). After: the worker processes asynchronously, main thread stays responsive, and highlighted tokens arrive 150ms after each keystroke with no input blocking. Perceived latency for typing dropped to <16ms.
Strategies Comparison
| Strategy | Latency | Priority | Thread | Use Case |
|---|---|---|---|---|
| ~0ms | Inherits caller | Main | Breaking up sequential work |
| ~0ms | Explicit | Main | Priority-based scheduling |
| 0-4ms | None | Main | Legacy yielding |
| ~0ms | None | Main | Zero-delay yielding (polyfill) |
| Variable | Lowest | Main | Non-urgent background work |
| Up to 16ms | Frame-aligned | Main | Visual updates only |
| Web Worker | Transfer overhead | N/A | Background | CPU-intensive parallel work |
Anti-Patterns
Processing entire large arrays synchronously.
items.forEach(heavyFn) on 10,000 items with 0.05ms per item creates a 500ms long task. Break into chunks with yielding between each chunk.
of multi-MB payloads on the main thread. JSON.parse
JSON.parse is synchronous and cannot be interrupted. A 5MB JSON string takes 50-200ms to parse depending on complexity. Use a streaming JSON parser in a Web Worker, or fetch data in smaller paginated chunks.
Using
for non-visual work. rAF callbacks share the frame budget with rendering. Scheduling heavy computation in rAF steals time from style, layout, and paint, causing frame drops. Use requestAnimationFrame
scheduler.postTask or requestIdleCallback for non-visual work.
Debounce-only approach to responsiveness. Debouncing a search input handler to 300ms hides the problem but does not fix it. When the debounced handler finally fires, it may still run a 200ms long task. Combine debouncing with chunked processing and yielding.
Assuming Web Worker communication is free.
postMessage uses the structured clone algorithm, which can be expensive for large objects. Transferring a 10MB ArrayBuffer is fast (transferable), but cloning a 10MB object graph with nested arrays takes 50-100ms. Use transferable objects or SharedArrayBuffer when possible.
Source
- Long Tasks API specification — https://w3c.github.io/longtasks/
- web.dev "Optimize long tasks" — https://web.dev/articles/optimize-long-tasks
- Scheduler API specification — https://wicg.github.io/scheduling-apis/
- Chrome DevTools Performance panel documentation — https://developer.chrome.com/docs/devtools/performance
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.
- No long tasks (>50ms) occur during critical user interactions.
- The Long Tasks API is used to detect and monitor long tasks in production.