DEV Community

Cover image for How to ship interactive charts and still aim for 100 performance
FSCSS for ProvChart

Posted on

How to ship interactive charts and still aim for 100 performance

Interactive charts and a perfect Lighthouse run often feel incompatible. The usual path is a 50–150 KB chart library, hydration, and a blank box until JavaScript finishes. You can still offer real visuals and light interaction without paying that tax on every page load.

This article is about a paint-first pipeline: compile data to HTML/CSS, let the browser draw on first paint, then add interaction as progressive enhancement.


The real performance problem

Core Web Vitals care about what blocks the main thread and what delays useful pixels.

Cost Typical JS chart stack Paint-first CSS charts
Download / parse Chart library + adapters No chart library for paint
First paint Often after JS runs Chart HTML/CSS in the first response
SEO / first crawl Depends on hydration Markup present immediately
Interaction Built into the library Optional small runtime

“100 performance” is never a guarantee—fonts, images, and third parties still matter—but removing chart-runtime JS from the critical path is one of the highest-leverage wins on dashboard-style pages.


Pipeline: compile → paint → enhance

1. Compile

Send JSON to a generator (build step or API). Example with ProvChart:

const res = await fetch("https://provchart-api.devtem.org/api/v1/generate", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": process.env.PROVCHART_API_KEY,
  },
  body: JSON.stringify({
    type: "area",
    series: [
      { name: "Traffic", color: "#4fd8c4", points: [30, 45, 40, 60, 55, 70, 65] },
      { name: "Signups", color: "#f0a860", points: [8, 12, 11, 18, 16, 22, 20] },
    ],
    axisX: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
  }),
});

const { success, html, css, error } = await res.json();
if (!success) throw new Error(error);
Enter fullscreen mode Exit fullscreen mode

You get scoped HTML + CSS (custom properties, clip-path, bars, gauges)—not a client chart framework.

2. Paint

Inject on the server or at the start of the page:

document.head.insertAdjacentHTML("beforeend", `<style>${css}</style>`);
document.getElementById("chart").innerHTML = html;
Enter fullscreen mode Exit fullscreen mode

On first paint the chart is already geometry in CSS. No new Chart(ctx, …).

3. Enhance (optional)

Interaction does not have to ride in the same bundle as rendering.

ProvChart Runtime is a separate MIT script: tooltips, scroll reveal, legend focus. Charts work without it.

<script>
  window.ProvChartRuntimeConfig = {
    tooltips: true,
    perPointTooltips: true,
    observe: true,
    excludeSvg: true,
  };
</script>
<script src="https://cdn.jsdelivr.net/npm/provchart-runtime@1.0.0/dist/provchart-runtime.min.js" defer></script>
Enter fullscreen mode Exit fullscreen mode

Pattern: render is free of chart-library JS; UX is progressive.


What “interactive” can mean without a heavy library

Need Lightweight approach
Hover values Runtime tooltips / CSS hits
Legend focus Dim other series on legend hover
Live data Rewrite CSS variables or re-call generate
README / docs generate-svg → commit .svg (no client runtime)
Full brush/zoom/analytics Reserve a JS library for that route only

Most marketing pages, pricing metrics, and ops KPI strips need the first column—not a full analytics suite.


Checklist toward a top performance score

  1. No chart library on the critical path — generate HTML/CSS ahead of time or via API; inline or ship only scoped CSS.
  2. Defer enhancementdefer / type="module" for runtime; charts remain visible if it fails.
  3. Respect prefers-reduced-motion — skip transform theatre when users ask.
  4. Keep images honest — LCP is often a hero image, not the chart; optimize that separately.
  5. Don’t block on fontsfont-display: swap for display type.
  6. Split the product — paint-first charts on public pages; heavy JS charts only behind “Analytics” where interaction earns the KB.
  7. Accessibility — short text summary + optional table (accessible charts).

Static sites and agents

  • SSG: generate charts at build time; commit HTML/CSS or call the API in CI.
  • README: prefer SVG file embeds over giant data URIs (Markdown guide).
  • Agents: stable JSON in → markup out is easy for ChatGPT/Claude tool calls—no browser chart API required.

When you should still use a JS chart library

  • Brushing, zooming, streaming ticks
  • Dozens of coordinated views
  • Exotic types your CSS engine doesn’t cover

Use them deliberately, on routes that justify the cost—not as the default for every sparkline.


Quick start

  1. Build or API-generate a chart → inject html + css.
  2. Confirm the chart shows with JS disabled (enhancement only).
  3. Add runtime only if you want tooltips/motion.
  4. Re-run Lighthouse on mobile; watch JS execution time and LCP.

Docs: chart.devtem.org/docs

Gallery: chart.devtem.org/gallery

Runtime: github.com/fscss-ttr/provchart-runtime


Maximize the visual by maximizing what the browser already paints. Compile data to CSS, ship interaction as an optional layer, and keep heavy chart runtimes off pages that don’t need them. That’s how interactive-looking charts stay compatible with serious performance budgets.


Top comments (0)