DEV Community

Selenium39
Selenium39

Posted on

Why your GPS file converter should not have a server

Why your GPS file converter should not have a server

Every online converter for fitness files works the same way: upload your activity, a server parses it, you download the result. For a GPX track that is a strange deal to accept. The file is a precise log of where you live, where you train, when you leave the house, and how hard your heart was working the whole time.

I want to make the case that for this class of problem the server is not just unnecessary — it is the wrong architecture, and removing it changes what you can promise your users.

The workload does not need a datacenter

Parsing a FIT file is a loop over binary records. Converting to GPX is string building. Merging is list concatenation plus a sort. Splitting is a walk with an accumulator. Computing distance, ascent and average heart rate is arithmetic over 13,000 points.

None of this needs more than a few milliseconds of CPU. A 700 KB activity file with 13,348 track points parses, converts and renders in well under a second on a mid-range phone. There is no GPU work, no model inference, no database, no concurrency problem.

The only thing a server adds is the ability to see the data. That is not a feature.

Files never have to leave memory

The browser gives you File.arrayBuffer(), which reads bytes straight into memory without a network call. There is nowhere to send them unless you write the code that does it.

const bytes = await file.arrayBuffer();
const track = parseAny(bytes, sniffFormat(new Uint8Array(bytes), file.name));
const gpx = serializeAny(track, 'gpx');
const blob = new Blob([gpx], { type: 'application/gpx+xml' });
Enter fullscreen mode Exit fullscreen mode

That is the whole pipeline. The download is a URL.createObjectURL away. Watch the network tab while doing it: no request carries file bytes, because no request was written.

The worker is the hard part

The one real engineering constraint is that parsing a 50 MB file on the main thread freezes the UI. The answer is a dedicated Web Worker: the main thread only handles drag-and-drop, progress and rendering; the worker does the parsing, conversion, merging and stats.

There is a trap here that cost me a release. A Worker has no window and no document, and therefore no DOMParser. GPX and TCX are XML, so the obvious parser throws:

ReferenceError: DOMParser is not defined
Enter fullscreen mode Exit fullscreen mode

FIT is unaffected because it is binary — you walk the header, read definition messages, decode records by field type. So the bug hides: everything looks fine if you only test with .fit input, and every XML path fails at runtime with an error the user reads as "this tool is broken."

Two clean fixes:

Parse in the main thread, compute in the worker. file.text() plus DOMParser on the main thread, map to a plain array of points, postMessage the structured data. The clone is cheap and serialization stays in the worker where it belongs.

Use a worker-safe XML parser. fast-xml-parser has no DOM dependency and keeps all parsing inside the worker. Faster on large tracks, more code to map Garmin's TrackPointExtension namespaces yourself.

I took the first option: one parse, structured data over the wire, no extra dependency.

Detail loss is the real problem, not speed

Formats are not equally expressive, and this is where a converter earns or loses trust:

Data FIT GPX TCX
Position + elevation yes yes yes
Heart rate yes via gpxtpx extension yes
Cadence / power yes via extensions via TPX
Laps / intervals full lost (segments only) yes
Session summary yes no yes

Heart rate and cadence survive into GPX through the Garmin TrackPointExtension, which is why Strava shows them on a converted file. Lap metadata does not survive FIT to GPX — only the segment boundaries do.

A converter that says this out loud is more useful than one showing a green checkmark. The user needs to know that an interval workout will flatten into one long activity, before they upload it to their coach.

Repairing a corrupt FIT file, locally

A full memory card, a battery death mid-ride or a bad transfer leaves a FIT file that Garmin Connect and Strava both refuse, while the ride sits inside it. The header says how long the file should be, the file carries two CRCs, and any mismatch makes a strict parser bail on message one.

Rebuilding is mechanical: rewrite the 14-byte header's data-length field, recompute the header CRC, walk the definition/record messages until the bytes run out keeping the last complete message, then recompute the trailing file CRC. On a real 700 KB activity file truncated to 90%, that recovered 11,667 record messages and dropped 11 unreadable bytes.

That is a few hundred lines of byte manipulation. It does not require a server either.

What this buys you

Not speed, though there is no queue and it works on a plane. Not cost, though there is no bill per conversion.

It buys an honest privacy claim. "We do not store your files" is a policy that can change, be violated by a misconfigured bucket, or be overridden by an acquisition. "There is no upload endpoint" is an architectural fact you can verify in devtools in ten seconds.

For GPS data specifically — home address, training schedule, favorite routes, race calendar, all in one file — that difference matters more than it does for a JPEG compressor.

The honest limits

A browser tool is excellent at parse-transform-serialize. It is not a track editor. Gap removal, duplicate-point cleanup, elevation re-sampling and editing individual points need real software: GPS Track Editor, Garmin BaseCamp or QGIS.

The right move is to say so in the documentation, build the narrow thing well, and let the user choose. That is what GPX Converter does — FIT/GPX/TCX conversion, merge, split, FIT repair and map preview, all client-side, no account, no upload.

Top comments (0)