Client-Side vs. Server-Side Conversion: Where Should the Logic Actually Run?
Romaji to Kana Converter: Astro
Chapter 5 · Client-Side vs. Server-Side Conversion: Where Should the Logic Actually Run?
Every chapter since the project began has left this question genuinely open — Chapter 1's own comparison table named it directly, and Chapter 4 closed with a tip-box promising this chapter would measure it rather than argue about it. This chapter does exactly that: it builds a real, working server-side alternative to Chapter 4's own client-side conversion, then benchmarks the two directly with real code rather than assuming which one is faster.
Building the Server-Side Alternative
Chapter 1 left this project in Astro's own default output: 'static' mode, deliberately, with no
adapter installed — nothing in the project needed on-demand rendering yet. This chapter is the first genuine
exception: a real API route that runs convert() on the server needs to execute per request, not
once at build time, which means it needs an adapter after all — verified back in Chapter 1 against Astro's
own documentation: on-demand rendering requires one, full stop.
The rest of the project stays static by default. Adding an adapter doesn't switch the whole site over to
output: 'server' — it just makes on-demand rendering available, opt-in per route via
that route's own export const prerender = false:
The route itself imports the exact same, unmodified convert function Chapters 2 and 3 already
built and tested — no second copy of the algorithm, no drift between the two versions:
prerender = false to a scratch page with no
adapter installed, specifically to trigger a real build failure and feel the requirement directly. This
chapter is the real, non-scratch version of that exact situation — the first genuine route in the whole
project that actually needs what that exercise was rehearsing for.
A Second Way to Call It
Chapter 4's own input box still calls convert() directly, unchanged. A second box, added to the
same page, calls the new endpoint instead — same shape in, same shape out, but through a real network
request this time:
Typing the same words into both boxes produces identical output — sushi, kyaku,
and konnichiwa all convert exactly the same way whether the request stays in the browser or
makes a real round trip to the server, because both boxes ultimately call the exact same
convert() function underneath. The only real difference between them is what it costs to get
there — measured directly below, not assumed.
Measuring the Real Difference
Rather than guessing, a small, real benchmark compares a direct, in-process call to convert()
against a real HTTP round trip carrying the identical JSON request/response shape this chapter's own
/api/convert route uses — a real local Node server and a real fetch client, run
for real rather than described in the abstract:
| Approach | Average time per call | 2,000-call total |
|---|---|---|
| Direct, in-process (Chapter 4's approach) | 1.36 microseconds | 2.72 ms |
| HTTP round trip to a local server | 14,084 microseconds (~14ms) | 28,168 ms |
That's a real, measured ~10,353× difference — re-run at a smaller sample size (500 calls) to check it wasn't a fluke, the per-call HTTP latency held steady at roughly the same ~14ms, with the ratio landing at ~7,018× that time. The exact multiple moves around a little run to run, since the direct call is fast enough to be sensitive to normal timer noise — but the real, load-bearing number, the ~14ms per HTTP round trip, was consistent across both runs.
What This Means for Chapter 4's Own Debouncing Question
Chapter 4's own tip-box named the exact condition under which debouncing would become worth doing: moving
convert() behind a real network call. That condition is now true for the server-side box built
in this chapter — converting on every keystroke through /api/convert means paying something in
the neighborhood of this chapter's own measured ~14ms, per letter typed, for a feature that costs
approximately 1.36 microseconds when kept client-side. A debounce delay on the server-side box specifically
would be a genuinely reasonable addition; Chapter 4's own client-side box still needs none, for the exact
reason it never did.
So Which Should This App Actually Use?
Given the real evidence gathered in this chapter, the honest answer for this specific app isn't "it depends" — it's a real, measured recommendation: Chapter 4's client-side approach is the objectively better choice for this feature, and the server-side route built in this chapter is worth having built and understood, not worth actually shipping as this app's own default.
- No real benefit from moving it server-side.
convert()touches no secret, no shared state, and no data any user shouldn't already have direct access to — none of the usual reasons a feature genuinely needs to run on a server apply here. - A real, measured cost for doing it anyway. ~10,000× slower per call isn't a rounding error — it's the kind of number that would show up as real, felt lag on a slower connection or a busier server, for a feature that's otherwise instant.
- Works with no server running at all. The client-side version keeps working the instant JavaScript loads, with no dependency on this project's own backend being reachable, deployed, or even online.
This is a genuinely different verdict from this course's own Angular & Express sibling, which commits to server-side conversion from its own first chapter — worth naming directly, since that course's own reason was structural (giving Angular a real backend it doesn't otherwise have), not a claim that server-side conversion is generally better. This chapter reaches the opposite conclusion for a completely different, and for this specific app more directly relevant, reason: real, measured evidence that this particular feature has nothing to gain from a server and a real, measured cost for adding one anyway.
/api/convert wasn't pointless even though it loses this comparison — it's the concrete
proof that Chapter 1's own "no database needed" architectural reasoning holds up under real measurement, not
just under a plausible-sounding argument. A future feature genuinely requiring server-side logic (real
persistence, a rate-limited third-party API call, anything needing a secret) now has a real, working
on-demand route pattern to build on directly, using exactly the same prerender = false
mechanism this chapter already exercised.
Hands-On Exercises
Add the Node adapter and the /api/convert route exactly as this chapter describes, wire up the second, server-calling input box, and confirm the server-side box produces identical output to Chapter 4's own client-side box for "sushi", "kyaku", and "konnichiwa".
📄 View solutionBuild a small local benchmark of your own — a real HTTP server exposing the same JSON contract, and a script that times both a direct convert() call and a real fetch call in a loop. Run it and report your own measured ratio, confirming it lands in the same general order of magnitude (thousands of times slower for the network call) this chapter reports.
📄 View solutionGiven this chapter's own real, measured evidence, argue in your own words why Chapter 4's client-side approach should remain this app's actual default. Then name one hypothetical feature this project could add later that would have a genuine, real reason to run server-side instead.
📄 View solutionChapter 5 Quick Reference
- The Node adapter — added for exactly one route; the rest of the project stays static by default
- /api/convert — a real on-demand route (export const prerender = false) calling the exact same, unmodified convert() from Chapters 2-3
- Measured, not assumed — a real local benchmark: ~1.36µs per direct call vs. ~14,084µs (~14ms) per HTTP round trip, a real ~10,353× difference, confirmed at a second sample size
- Localhost isn't free — connection handling, double JSON serialization, and event-loop round trips cost real time even with zero network latency involved
- Debouncing now matters — for the server box only — Chapter 4's client-side box still needs none
- The real verdict — client-side is the objectively better choice for this specific feature; the server route stands as a working pattern for a future feature that actually needs one
- Next chapter: Styling & Matching the Existing Site's Own Look