This guide shows how to integrate @cloudflare/speedtest into a React application. The pattern uses a custom hook to manage the engine lifecycle and exposes results as React state.
The SDK relies on the PerformanceResourceTiming browser API, which is not available in server-side rendering (SSR) environments such as Next.js. Either import the engine inside a useEffect, or use dynamic import with { ssr: false }.
The useSpeedTest hook
import { useState, useEffect, useRef } from 'react';
import SpeedTest from '@cloudflare/speedtest';
function useSpeedTest() {
const [results, setResults] = useState(null);
const [isRunning, setIsRunning] = useState(false);
const [isFinished, setIsFinished] = useState(false);
const engineRef = useRef(null);
function start() {
const engine = new SpeedTest({ autoStart: false });
engine.onRunningChange = running => setIsRunning(running);
engine.onResultsChange = () => {
setResults(engine.results.getSummary());
};
engine.onFinish = finalResults => {
setResults(finalResults.getSummary());
setIsFinished(true);
};
engineRef.current = engine;
engine.play();
}
function pause() {
engineRef.current?.pause();
}
function restart() {
setIsFinished(false);
setResults(null);
engineRef.current?.restart();
}
return { results, isRunning, isFinished, start, pause, restart };
}
The hook manages three pieces of state:
| State | Description |
|---|
results | Latest getSummary() snapshot; updated after each measurement. |
isRunning | Mirrors engine.isRunning; true while transfers are active. |
isFinished | true once all configured measurements have completed. |
function SpeedTestWidget() {
const { results, isRunning, isFinished, start, pause, restart } = useSpeedTest();
const fmt = (bps) =>
bps != null ? `${(bps / 1e6).toFixed(1)} Mbps` : '—';
const fmtMs = (ms) =>
ms != null ? `${ms.toFixed(1)} ms` : '—';
return (
<div>
{/* Controls */}
<div>
{!isRunning && !isFinished && (
<button onClick={start}>Start test</button>
)}
{isRunning && (
<button onClick={pause}>Pause</button>
)}
{!isRunning && isFinished && (
<button onClick={restart}>Run again</button>
)}
{!isRunning && !isFinished && results && (
<button onClick={restart}>Restart</button>
)}
</div>
{/* Live metrics */}
{results && (
<table>
<tbody>
<tr>
<th>Download</th>
<td>{fmt(results.download)}</td>
</tr>
<tr>
<th>Upload</th>
<td>{fmt(results.upload)}</td>
</tr>
<tr>
<th>Latency</th>
<td>{fmtMs(results.latency)}</td>
</tr>
<tr>
<th>Jitter</th>
<td>{fmtMs(results.jitter)}</td>
</tr>
</tbody>
</table>
)}
{/* AIM scores — only available after the test finishes */}
{isFinished && results && <AimScores engine={engineRef} />}
</div>
);
}
results holds the output of getSummary(), which returns bandwidth in bps. Divide by 1e6 before displaying to users, as shown in the fmt helper above.
Displaying AIM scores
getScores() is only available after the engine has finished all measurements. It returns an object keyed by use-case category, each with a numeric score and a named classification:
function AimScores({ engine }) {
const scores = engine.current?.results.getScores();
if (!scores) return null;
return (
<div>
<h3>Performance scores</h3>
{Object.entries(scores).map(([category, score]) => (
<div key={category}>
<strong>{category}</strong>:{' '}
{score.points} points —{' '}
<span data-class={score.classificationName}>
{score.classificationName}
</span>
</div>
))}
</div>
);
}
Each entry in the scores object has this shape:
{
points: number;
classificationIdx: 0 | 1 | 2 | 3 | 4;
classificationName: 'bad' | 'poor' | 'average' | 'good' | 'great';
}
SSR-safe usage (Next.js)
Because the SDK requires browser APIs, it must not run during server-side rendering. Two approaches work:
useEffect import
Next.js dynamic import
import { useState, useEffect, useRef } from 'react';
function useSpeedTest() {
const [SpeedTest, setSpeedTest] = useState(null);
const engineRef = useRef(null);
useEffect(() => {
// Only import on the client
import('@cloudflare/speedtest').then(mod => {
setSpeedTest(() => mod.default);
});
}, []);
function start() {
if (!SpeedTest) return;
const engine = new SpeedTest({ autoStart: false });
engineRef.current = engine;
engine.play();
}
return { start };
}
// pages/speedtest.jsx (or app/speedtest/page.jsx)
import dynamic from 'next/dynamic';
const SpeedTestWidget = dynamic(
() => import('../components/SpeedTestWidget'),
{ ssr: false }
);
export default function Page() {
return <SpeedTestWidget />;
}
This tells Next.js to skip server-rendering SpeedTestWidget entirely, which is the simplest approach if the entire component tree depends on the SDK.
Complete example
Putting it all together — install the package, then use the hook and component above:
Install the package
npm install @cloudflare/speedtest
Create the hook file
Copy the useSpeedTest hook into src/hooks/useSpeedTest.js (or .ts).
Build the component
Use SpeedTestWidget as a client component. In the Next.js App Router, add 'use client' at the top of the file.'use client';
import { useSpeedTest } from '../hooks/useSpeedTest';
export function SpeedTestWidget() {
// ... (component body from above)
}
Handle cleanup on unmount
If the component can unmount while a test is running, pause and discard the engine reference:useEffect(() => {
return () => {
engineRef.current?.pause();
engineRef.current = null;
};
}, []);