← Back to portfolio

Aug 2026 · 3 min read · Next.js · Performance · Tailwind

Building a QR Ordering System for Low-End Devices

When I built the Restaurant POS, the hardest constraint wasn't the feature list — it was the target hardware. The users I care about run cheap Android phones with 2GB of RAM and weak GPUs, often operating on flaky, high-latency networks. A default Next.js + Tailwind setup ships far more JavaScript and layout work than those devices can handle, meaning the initial prototype was slow to boot and janky when scrolling through a 200-item menu.

Start with the constraint, not the stack

Before writing a single line of code, I established a strict performance budget:

This framing drove every technical decision that followed.

Virtualization keeps memory flat

Rendering a 200-item menu creates 200 heavy DOM nodes plus event listeners if rendered all at once. Instead, virtualization ensures only the items near the active viewport live in the DOM. With a window of roughly 12–15 rows, the rendered node count stays constant no matter how long the menu grows. The practical gain is that scrolling performance stops scaling with catalog size.

Lazy loading that respects the device

Images use loading="lazy" combined with decoding="async" so photos only decode when they enter the viewport, keeping the cart lightweight until checkout.

The one critical trap: The Largest Contentful Paint (LCP) image must not be lazy-loaded, or the browser delays parsing and first paint suffers. That is why the eager fetchpriority pattern belongs strictly on the above-the-fold hero element.

Avoiding layout thrash on weak GPUs

Scroll-induced layout recalculations are the silent killer on low-end devices. Two quick wins solved this:

  1. Assigning fixed heights to media elements in CSS so images can't push surrounding content around mid-load.
  2. Utilizing content-visibility: auto on long offscreen sections so the browser skips layout calculations until elements are near the viewport.

The result is fewer recalculation passes, keeping frames locked at 60fps.

Optimistic cart, resilient orders

Kitchen workflows cannot stall on a slow network. The cart applies updates optimistically (delivering zero perceived latency to the kitchen queue), while order states are secured via server-side confirmation and resilient database retries written to PostgreSQL via Prisma.

Choosing package versions intentionally

For the mobile menu, I chose HeroUI over heavier component kits because fast primitives and low bundle weight mattered more than a massive widget catalog. On Trader POS — a dense desktop web app — I deliberately used shadcn/ui + Tailwind. Different products require different trade-offs, and understanding that balance is a key engineering signal.


See the live demo in action: Restaurant POS.