Long Task Widget

A compact main-thread timeline that paints each scheduled task as a horizontal block, colour-coded by whether it crosses the Long Tasks API threshold (50ms by default). Drop it into an INP lesson, a scheduler.yield() walkthrough, or any "why is my page janky" article.

Preview

Before yielding

Two long tasks hold the main thread for the full window.

400ms window2 long tasks
Customize
Timeline
400ms
50ms
50ms
Options

Installation

npx shadcn@latest add https://craftbits.dev/r/long-task-widget.json

Usage

import { LongTaskWidget } from "@craft-bits/core";
 
<LongTaskWidget
  title="Before yielding"
  windowMs={400}
  tasks={[
    { id: "hydrate", start: 0, duration: 280, label: "hydrate — 280ms" },
    { id: "parse", start: 280, duration: 120, label: "parse — 120ms" },
  ]}
/>

Chunk the same work to land under the 50ms threshold:

<LongTaskWidget
  title="After yielding"
  windowMs={400}
  tasks={Array.from({ length: 8 }, (_, i) => ({
    id: "chunk-" + i,
    start: i * 50,
    duration: 50,
  }))}
/>

Anatomy

  • Header. Optional title (renders with the cb-label style) and a description sub-line. Omit both for a chromeless panel.
  • Track. A single horizontal track sized by windowMs. Each task is positioned absolutely by its start and sized by its duration.
  • Block tone. Tasks longer than longTaskThresholdMs paint with --cb-error; the rest paint with --cb-success. Each block carries data-cb-verdict="long" | "short" so consumers can extend the palette.
  • Legend. A trailing chip summarises the long-task count. Hide it with hideLegend.

Understanding the component

  1. Layout. Each task renders as an absolute block. left is derived from start / windowMs, width from duration / windowMs. The widget infers windowMs from the latest task end when the prop is omitted.
  2. Threshold. longTaskThresholdMs defaults to 50 — the Long Tasks API definition. Override it for stricter budgets (e.g. 34 for the 30 FPS frame budget) or for educational "what if we shrank the threshold" demos.
  3. Tone. Tasks above the threshold tint --cb-error; everything else tints --cb-success. The chip in the footer mirrors the worst verdict in the timeline.
  4. Motion. Each block fades and scaleX-springs in once on mount (spring snap). The animation short-circuits under prefers-reduced-motion.

Props

PropTypeDefaultDescription
tasksLongTaskWidgetTask[]requiredOrdered list of task blocks.
titleReactNodeOptional heading above the track.
descriptionReactNodeOptional sub-headline under the title.
windowMsnumberinferredTotal width of the timeline in ms.
longTaskThresholdMsnumber50Threshold above which a task reads as long.
hideLegendbooleanfalseHide the legend chip in the footer.
headingAs'h2' | 'h3' | 'h4''h3'Tag for the title element.
classNamestringMerged onto the root via cn().

LongTaskWidgetTask

FieldTypeDescription
idstringStable identifier.
startnumberOffset in ms from the start of the timeline.
durationnumberLength of the task in ms.
labelReactNodeOptional in-block label. Defaults to the duration in ms.

Accessibility

  • The wrapper is a <section> with data-cb-edu="long-task-widget". The track is a role="img" with an aria-label summarising the task count, window width, and long-task verdict.
  • Each block carries a descriptive aria-label so screen-reader users get the same picture as sighted users.
  • Each block exposes data-cb-verdict="long" | "short" for consumers extending the palette without monkey-patching CSS.
  • Animations are limited to the block enter and short-circuit under prefers-reduced-motion.

Credits

  • Extracted from: terminal-dreams (src/components/frontend-design/sdp-web-performance/ui/LongTaskWidget.tsx). The original was wired to a project-level PerfContext, hard-coded a before/after row, and embedded a click simulator. This rewrite drops every project-specific prop and the simulator chrome — consumers pass their own tasks array, so the widget visualises any timeline of main-thread work, not just the SDP web-performance lesson.