Skip to content

Commit c867e68

Browse files
committed
perf: eliminate redundant chart redraws on initial load
- Skip chart.update() loop on first init (setOptions already covers new charts) - Debounce MutationObserver/mediaQuery callbacks (100ms) to coalesce rapid attr changes - Primer sets data-color-mode during mount which triggered an unnecessary full redraw
1 parent 9c2cb2e commit c867e68

File tree

1 file changed

+36
-22
lines changed

1 file changed

+36
-22
lines changed

src/components/charts/useHighchartsInit.ts

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import { useEffect } from 'react';
22

33
let initialized = false;
4+
let themeDebounceTimer: ReturnType<typeof setTimeout> | null = null;
45

5-
/** Apply the GitHub Highcharts theme using live CSS variable values */
6-
async function initAndApplyTheme() {
6+
/**
7+
* Apply the GitHub Highcharts theme using live CSS variable values.
8+
* @param updateExisting - When true, re-applies theme to already-mounted charts.
9+
* Skipped on first init because `Highcharts.setOptions()` already covers
10+
* charts created after the call. Only needed for live theme switches.
11+
*/
12+
async function initAndApplyTheme(updateExisting = false) {
713
const [{ default: Highcharts }, { buildGitHubChartTheme }] = await Promise.all([
814
import('highcharts'),
915
import('../../lib/chart-theme'),
@@ -23,15 +29,29 @@ async function initAndApplyTheme() {
2329
plotOptions: { series: { animation: false } },
2430
});
2531

26-
// Re-apply visual styles (colors, backgrounds) to existing charts
27-
// without clobbering per-chart options like title, series, etc.
28-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
29-
const { title: _t, subtitle: _s, series: _sr, ...safeTheme } = theme;
30-
Highcharts.charts.forEach((chart) => {
31-
if (chart) {
32-
chart.update(safeTheme, true, true);
33-
}
34-
});
32+
// Only re-apply to existing charts on theme change (not first init)
33+
if (updateExisting) {
34+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
35+
const { title: _t, subtitle: _s, series: _sr, ...safeTheme } = theme;
36+
Highcharts.charts.forEach((chart) => {
37+
if (chart) {
38+
chart.update(safeTheme, true, true);
39+
}
40+
});
41+
}
42+
}
43+
44+
/**
45+
* Debounced theme re-apply for live color scheme changes.
46+
* Coalesces rapid attribute mutations (Primer sets multiple attrs at once)
47+
* into a single chart update pass.
48+
*/
49+
function scheduleThemeUpdate() {
50+
if (themeDebounceTimer) clearTimeout(themeDebounceTimer);
51+
themeDebounceTimer = setTimeout(() => {
52+
themeDebounceTimer = null;
53+
initAndApplyTheme(true);
54+
}, 100);
3555
}
3656

3757
/** Initialize Highcharts with GitHub theme + accessibility module.
@@ -40,28 +60,22 @@ export function useHighchartsInit() {
4060
useEffect(() => {
4161
if (!initialized) {
4262
initialized = true;
43-
initAndApplyTheme();
63+
initAndApplyTheme(false); // First init — no existing charts to update
4464
}
4565

4666
// Watch for OS color scheme changes (light ↔ dark)
4767
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
48-
const handleChange = () => {
49-
// Small delay to let Primer's theme CSS vars update first
50-
setTimeout(() => { initAndApplyTheme(); }, 50);
51-
};
52-
mediaQuery.addEventListener('change', handleChange);
68+
mediaQuery.addEventListener('change', scheduleThemeUpdate);
5369

54-
// Also watch for Primer's data-color-mode attribute changes
55-
const observer = new MutationObserver(() => {
56-
setTimeout(() => { initAndApplyTheme(); }, 50);
57-
});
70+
// Watch for Primer's data-color-mode attribute changes
71+
const observer = new MutationObserver(scheduleThemeUpdate);
5872
const primerRoot = document.querySelector('[data-color-mode]');
5973
if (primerRoot) {
6074
observer.observe(primerRoot, { attributes: true, attributeFilter: ['data-color-mode', 'data-light-theme', 'data-dark-theme'] });
6175
}
6276

6377
return () => {
64-
mediaQuery.removeEventListener('change', handleChange);
78+
mediaQuery.removeEventListener('change', scheduleThemeUpdate);
6579
observer.disconnect();
6680
};
6781
}, []);

0 commit comments

Comments
 (0)