Dhaka Speed Map: User reported internet speeds in Dhaka
This project aggregates speed test results submitted by visitors on the site. The goal is to map out the difference between speeds advertised by ISPs and the actual speeds experienced by users.
Google Maps embedded with markers for each speed test result. The markers are color coded based on the speed test result.
Data is stored and retrieved from Firebase Firestore.
export type SpeedPoint = {
key: string;
isp: string; // User's Internet Service Provider
advertised: number; // Advertised speed in Mbps
download: number; // Reported download speed in Mbps
upload: number; // Reported upload speed in Mbps
lat: number; // Latitude
lng: number; // Longitude
note: string; // Additional feedback
};
A 30 MB file is hosted on a GitHub Pages site with a custom domain. The custom domain allows CORS requests, which is necessary for the speed test to work.
const response = await fetch(
`https://speed-test-files.zzzzion.com/data-30-mb.txt?timestamp=${Date.now()}`
);
const downloadData = await response.blob(); // Get the file as a Blob
The fetch request is timed to measure the download speed.
The same file is then uploaded to a Cloudflare Worker, hosted at speed-test-upload.zzzzion.workers.dev.
addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
if (request.method === "POST") {
const formData = await request.formData();
const file = formData.get("file");
return new Response("File uploaded and processed", { status: 200 });
}
return new Response("Method not allowed", { status: 405 });
}
This measures the time it takes to upload the file to Cloudflare's Edge network.
User's network speed is testing by generating dummy 25 MB data using a Next.js API route. The frontend requests the speed test endpoint, and measures the time it takes to download the data. It then uploads the same data back to the server to measure the upload speed.
It is worth noting that the Speedtest.net API was preferred but was not compatible with the Next.js serverless environment.
The user's ISP is detected using the ipinfo.io API.
const response = await fetch(
`https://ipinfo.io/json?token=${process.env.NEXT_PUBLIC_IP_INFO_API_KEY}`
);
if (!response.ok) {
throw new Error("Failed to fetch ISP info.");
}
const data = await response.json();
return data.org.split(" ").slice(1).join(" ");
There's a default list of ISPs in Firestore. If a new ISP is detected, the database is automatically updated, and this new ISP is added to the dropdown menu for future submissions.
- Green: reported speed close or equal to advertised speed
- Yellow: reported speed is less than advertised speed
- Red: reported speed is significantly less than advertised speed
The markers are memoized to prevent re-rendering on each state change. The
memoization is done using the useMemo
hook.
React.memo()
is a higher-order component that memoizes the rendered output of
the wrapped component. The MemoizedMarker
components only re-render when their
specific props change.
const MemoizedMarker = React.memo(({ point, setMarkerRef, ... }) => (...));
useMemo()
hook memoizes the result of a computation. In this case, the
computation is the creation of the points array. The memoizedPoints array
reference only changes when the actual data changes.
const memoizedPoints = useMemo(() => points, [points]);
useCallback()
hook returns a memoized version of the callback function that
only changes if one of the dependencies has changed. It prevents unnecessary
re-creation of these functions on every render of the Markers component.
// setMarkerRef
const setMarkerRef = useCallback(
(marker: Marker | null, key: string) => {
// ... function body ...
},
[markers]
);
// ratioToColor
const ratioToColor = useCallback((advertised: number, download: number) => {
// ... function body ...
}, []);
The markers are clustered using the react-google-maps
library. The
MarkerClusterer
component groups the markers based on the zoom level of the
map.
Dropdown to filter results by ISP.
Self report form to add visitor's speed test results. The form includes fields for:
- ISP
- Advertised speed
- Reported speed (download and upload)
GitHub Globe UI Component from Aceternity UI.
It was surprisingly difficult to maintain state between the dropdown menu and the map markers. The markers were not updating correctly when the dropdown menu was changed. With help from ChatGPT, the issue was resolved by using memoization to prevent unnecessary re-renders.