Skip to content

Commit 9383cc5

Browse files
author
@bronzeagecto
committed
Performance optimizations for mobile
1 parent be1d7c1 commit 9383cc5

File tree

1 file changed

+62
-43
lines changed

1 file changed

+62
-43
lines changed

src/js/graph.ts

Lines changed: 62 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -131,20 +131,22 @@ interface HashTarget {
131131
element: Selection<SVGGElement, GraphNode, SVGGElement, unknown>;
132132
}
133133

134-
function initGlyphs(rootElement: Element): void {
134+
function initGlyphs(rootElement: Element, options?: { reduced?: boolean }): void {
135135
const glyphContainer = rootElement.querySelector('.graph-glyphs');
136136
if (!glyphContainer) {
137137
return;
138138
}
139139

140+
const reduced = Boolean(options?.reduced);
140141
const glyphSets = [
141142
'∆ ƒ π λ ξ 0 1 2 3 5 8 A C E G H K',
142143
'β γ ζ ψ Ω 7 9 B D F J L N Q Z',
143144
'▮ ▯ ▰ ϟ ψ Ω ≡ ≣ ≠ ✶ ✷ ✸ ✹ ✺',
144145
'0 1 0 1 0 1 0 1 0 1 0 1 0 1',
145146
];
146147

147-
const totalClusters = 16;
148+
const totalClusters = reduced ? 6 : 16;
149+
const baseTimeout = reduced ? 2600 : 1500;
148150
const fragment = document.createDocumentFragment();
149151
const clusters: HTMLElement[] = [];
150152

@@ -161,9 +163,9 @@ function initGlyphs(rootElement: Element): void {
161163
function randomizeCluster(cluster: HTMLElement): void {
162164
const left = Math.random() * 100;
163165
const top = Math.random() * 100;
164-
const scale = 0.6 + Math.random() * 1.4;
165-
const flicker = `${3.5 + Math.random() * 3}s`;
166-
const hue = `${6 + Math.random() * 5}s`;
166+
const scale = reduced ? 0.8 + Math.random() * 0.8 : 0.6 + Math.random() * 1.4;
167+
const flicker = reduced ? `${4.5 + Math.random() * 2.5}s` : `${3.5 + Math.random() * 3}s`;
168+
const hue = reduced ? `${8 + Math.random() * 4}s` : `${6 + Math.random() * 5}s`;
167169
const text = glyphSets[Math.floor(Math.random() * glyphSets.length)];
168170

169171
cluster.style.setProperty('left', `${left}%`);
@@ -192,7 +194,7 @@ function initGlyphs(rootElement: Element): void {
192194
cluster.style.transition = 'opacity 0.35s ease';
193195
cluster.style.opacity = '0';
194196

195-
const timeout = 1500 + Math.random() * 2500;
197+
const timeout = baseTimeout + Math.random() * (reduced ? 2200 : 2500);
196198

197199
window.setTimeout(() => {
198200
randomizeCluster(cluster);
@@ -201,7 +203,7 @@ function initGlyphs(rootElement: Element): void {
201203
}
202204

203205
for (let i = 0; i < clusters.length; i += 1) {
204-
const delay = (i / clusters.length) * 2000;
206+
const delay = reduced ? 0 : (i / clusters.length) * 2000;
205207
window.setTimeout(() => loopCluster(i), delay);
206208
}
207209
}
@@ -212,7 +214,14 @@ export async function initGraph(containerSelector: string, dataUrl: string): Pro
212214
return;
213215
}
214216

215-
initGlyphs(rootElement);
217+
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
218+
const isSmallScreen = window.matchMedia('(max-width: 768px)').matches;
219+
const extendedNavigator = navigator as Navigator & { connection?: { saveData?: boolean } };
220+
const hasSaveDataPreference = Boolean(extendedNavigator.connection?.saveData);
221+
const isLowConcurrency = typeof navigator.hardwareConcurrency === 'number' && navigator.hardwareConcurrency > 0 && navigator.hardwareConcurrency <= 4;
222+
const shouldReduceMotion = prefersReducedMotion || hasSaveDataPreference || isSmallScreen || isLowConcurrency;
223+
224+
initGlyphs(rootElement, { reduced: shouldReduceMotion });
216225

217226
const response = await fetch(dataUrl);
218227
if (!response.ok) {
@@ -263,32 +272,35 @@ export async function initGraph(containerSelector: string, dataUrl: string): Pro
263272
default:
264273
customColor = NODE_COLORS[node.type];
265274
}
266-
nodes.push({
275+
const nodeEntry: GraphNode = {
267276
...node,
268277
radius,
269278
chargeStrength,
270279
color: customColor,
271-
pulse: {
280+
spinAngle: 0,
281+
};
282+
if (!shouldReduceMotion) {
283+
nodeEntry.pulse = {
272284
baseRadius: radius,
273285
amplitude: radius * ((PULSE_SCALE[node.type] ?? 1.7) - 1) * (0.6 + Math.random() * 1.2),
274286
speed: 0.003 + Math.random() * 0.005,
275287
phase: Math.random() * Math.PI * 2,
276288
baseCharge: chargeStrength,
277-
},
278-
drift: {
289+
};
290+
nodeEntry.drift = {
279291
amplitudeX: 18 + Math.random() * 26,
280292
amplitudeY: 12 + Math.random() * 18,
281293
speedX: 0.00008 + Math.random() * 0.00012,
282294
speedY: 0.00008 + Math.random() * 0.00012,
283295
phaseX: Math.random() * Math.PI * 2,
284296
phaseY: Math.random() * Math.PI * 2,
285-
},
286-
spin: {
297+
};
298+
nodeEntry.spin = {
287299
speed: 0.00012 + Math.random() * 0.00025,
288300
phase: Math.random() * Math.PI * 2,
289-
},
290-
spinAngle: 0,
291-
});
301+
};
302+
}
303+
nodes.push(nodeEntry);
292304
}
293305

294306
for (let i = 0; i < data.links.length; i += 1) {
@@ -321,18 +333,20 @@ export async function initGraph(containerSelector: string, dataUrl: string): Pro
321333
linkGradient.append('stop').attr('offset', '55%').attr('stop-color', 'rgba(113, 213, 255, 0.52)');
322334
linkGradient.append('stop').attr('offset', '100%').attr('stop-color', 'rgba(140, 91, 250, 0.55)');
323335

324-
const glowFilter = defs
325-
.append('filter')
326-
.attr('id', 'node-glow')
327-
.attr('x', '-50%')
328-
.attr('y', '-50%')
329-
.attr('width', '200%')
330-
.attr('height', '200%');
331-
332-
glowFilter.append('feGaussianBlur').attr('stdDeviation', 18).attr('result', 'coloredBlur');
333-
const feMerge = glowFilter.append('feMerge');
334-
feMerge.append('feMergeNode').attr('in', 'coloredBlur');
335-
feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
336+
if (!shouldReduceMotion) {
337+
const glowFilter = defs
338+
.append('filter')
339+
.attr('id', 'node-glow')
340+
.attr('x', '-50%')
341+
.attr('y', '-50%')
342+
.attr('width', '200%')
343+
.attr('height', '200%');
344+
345+
glowFilter.append('feGaussianBlur').attr('stdDeviation', 18).attr('result', 'coloredBlur');
346+
const feMerge = glowFilter.append('feMerge');
347+
feMerge.append('feMergeNode').attr('in', 'coloredBlur');
348+
feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
349+
}
336350

337351
const zoomBehavior = zoom<SVGSVGElement, unknown>()
338352
.scaleExtent([0.5, 2])
@@ -356,9 +370,12 @@ export async function initGraph(containerSelector: string, dataUrl: string): Pro
356370
.attr('stroke-opacity', 0.72)
357371
.attr('stroke', 'url(#link-gradient)');
358372

359-
const nodeSelection = g
360-
.append('g')
361-
.attr('filter', 'url(#node-glow)')
373+
const nodesGroup = g.append('g');
374+
if (!shouldReduceMotion) {
375+
nodesGroup.attr('filter', 'url(#node-glow)');
376+
}
377+
378+
const nodeSelection = nodesGroup
362379
.selectAll<SVGGElement, GraphNode>('g')
363380
.data(nodes)
364381
.join('g')
@@ -400,14 +417,16 @@ export async function initGraph(containerSelector: string, dataUrl: string): Pro
400417
.attr('opacity', 0.92)
401418
.attr('data-node-id', (d) => d.id);
402419

403-
nodeSelection
404-
.append('circle')
405-
.attr('r', (d) => (d.radius ?? NODE_RADIUS[d.type]) * 1.25)
406-
.attr('fill', 'none')
407-
.attr('stroke', (d) => (d as GraphNode & { color?: string }).color ?? NODE_COLORS[d.type])
408-
.attr('stroke-width', 3.5)
409-
.attr('opacity', 0.36)
410-
.attr('class', 'graph-node__glow');
420+
if (!shouldReduceMotion) {
421+
nodeSelection
422+
.append('circle')
423+
.attr('r', (d) => (d.radius ?? NODE_RADIUS[d.type]) * 1.25)
424+
.attr('fill', 'none')
425+
.attr('stroke', (d) => (d as GraphNode & { color?: string }).color ?? NODE_COLORS[d.type])
426+
.attr('stroke-width', 3.5)
427+
.attr('opacity', 0.36)
428+
.attr('class', 'graph-node__glow');
429+
}
411430

412431
nodeSelection
413432
.append('text')
@@ -445,10 +464,10 @@ export async function initGraph(containerSelector: string, dataUrl: string): Pro
445464
}),
446465
)
447466
.force('center', forceCenter(width / 2, height / 2))
448-
.force('collision', forceCollide().radius((d) => (d.radius ?? 10) + 28).strength(1.5))
467+
.force('collision', forceCollide().radius((d) => (d.radius ?? 10) + (shouldReduceMotion ? 18 : 28)).strength(shouldReduceMotion ? 1 : 1.5))
449468
.force('x', forceX(width / 2).strength(0.008))
450469
.force('y', forceY(height / 2).strength(0.008))
451-
.alphaDecay(0.04);
470+
.alphaDecay(shouldReduceMotion ? 0.08 : 0.04);
452471

453472
simulation
454473
.force('type-position', forceY<GraphNode>().strength(0.035).y((d) => TYPE_FORCE_TARGETS[d.type] ?? height / 2));
@@ -634,7 +653,7 @@ export async function initGraph(containerSelector: string, dataUrl: string): Pro
634653
return null;
635654
});
636655

637-
simulation.alpha(0.2).restart();
656+
simulation.alpha(shouldReduceMotion ? 0.1 : 0.2).restart();
638657
}
639658

640659
nodeSelection

0 commit comments

Comments
 (0)