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);
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;
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>
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
- No chart library on the critical path — generate HTML/CSS ahead of time or via API; inline or ship only scoped CSS.
-
Defer enhancement —
defer/type="module"for runtime; charts remain visible if it fails. -
Respect
prefers-reduced-motion— skip transform theatre when users ask. - Keep images honest — LCP is often a hero image, not the chart; optimize that separately.
-
Don’t block on fonts —
font-display: swapfor display type. - Split the product — paint-first charts on public pages; heavy JS charts only behind “Analytics” where interaction earns the KB.
- 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
- Build or API-generate a chart → inject
html+css. - Confirm the chart shows with JS disabled (enhancement only).
- Add runtime only if you want tooltips/motion.
- 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)