Category: Web

  • Why We Migrated a 200k-User SaaS from CRA to Next.js App Router

    When Fluxen’s dashboard started shipping features weekly, our Create React App setup became the bottleneck. Cold builds crept past 90 seconds, bundle sizes ballooned past 2 MB, and Lighthouse scores on the marketing pages hovered in the 50s. Moving to Next.js App Router wasn’t a weekend project — it took six weeks and touched every corner of the codebase.

    The trigger

    The final straw was a client demo where the app took 8 seconds to become interactive on a 4G connection. We’d been papering over performance with skeleton loaders and optimistic UI, but we were losing the race. Next.js offered server components, streaming SSR, and built-in image optimisation — exactly what we needed.

    What we migrated first

    We started with the public-facing pages (marketing, pricing, docs) because they had the clearest performance story. Moving those to static generation with generateStaticParams cut time-to-first-byte from ~600 ms to under 80 ms. The wins were immediate and visible.

    The authenticated dashboard was harder. We had hundreds of components with useEffect-heavy data fetching. We adopted a “server shell, client leaf” pattern: layout and navigation became server components; interactive widgets stayed as client components. This reduced the client bundle by 40%.

    What broke

    Context providers that lived at the app root needed to move inside a 'use client' boundary wrapper. Several third-party libraries (a charting lib and a drag-and-drop package) threw hydration errors until we wrapped them in dynamic imports with ssr: false.

    Results after 8 weeks in production

    • Lighthouse performance score: 54 → 91
    • JS bundle (initial): 2.1 MB → 780 KB
    • Build time (CI): 94 s → 31 s
    • Support tickets about slowness: down 70%

    The migration was painful but worth it. If you’re still on CRA, start with your public pages — the wins are fast and build the team’s confidence for the harder dashboard work ahead.

  • Building Accessible Navigation: What WCAG 2.2 Actually Requires

    Navigation is the most-audited part of any website, and the most commonly failed. After running accessibility audits on a dozen client projects in the past year, the same three issues appear almost every time: missing skip links, keyboard traps in mobile menus, and focus indicators that exist only in the browser’s default stylesheet.

    Skip links

    A “Skip to main content” link must be the first focusable element on the page. It can be visually hidden until focused — but it must be reachable by keyboard and functional. The most common mistake is display: none, which removes it from the focus order entirely. Use the clip pattern instead:

    .skip-link {
      position: absolute;
      transform: translateY(-100%);
    }
    .skip-link:focus {
      transform: translateY(0);
    }

    Mobile menu keyboard traps

    When a mobile menu opens, focus should move inside it. When it closes, focus must return to the trigger button. Failing to return focus leaves keyboard users stranded at the top of the page. We use a small useFocusTrap hook that captures Tab and Shift+Tab while the menu is open.

    Focus indicators

    WCAG 2.2 introduced Success Criterion 2.4.11 (Focus Appearance — minimum), which requires a focus indicator with at least 3:1 contrast against adjacent colours and an area of at least the element’s perimeter. A safe cross-browser pattern:

    :focus-visible {
      outline: 2px solid #FFB020;
      outline-offset: 3px;
    }

    Accessibility isn’t a project-end checklist item — it’s cheapest to build in from the first component. These three fixes alone will clear the majority of navigation findings in most audits.

  • Practical Guide to Web Performance Budgets on Client Projects

    Most web agencies know they should have performance budgets. Few actually enforce them. After a project where a last-minute carousel widget pushed a client’s LCP past 4 seconds on launch day, we made budgets a contractual deliverable — not a nice-to-have.

    What goes into a budget

    We set three numbers per project:

    • LCP ≤ 2.5 s on a simulated mid-tier Android device over 4G (Lighthouse mobile preset)
    • Total JS ≤ 200 KB (compressed) for the initial route
    • CLS < 0.1 — no layout shift after fonts load

    These map directly to Core Web Vitals thresholds, which keeps the conversation simple with clients who care about SEO.

    Enforcement in CI

    We use bundlesize for the JS limit and a Lighthouse CI step for LCP and CLS. Both run on every pull request. A failed check blocks the merge — no exceptions, no “we’ll fix it later.” The key is configuring the Lighthouse CI budget file at project kickoff, not after launch.

    {
      "budgets": [
        {
          "resourceSizes": [{ "resourceType": "script", "budget": 200 }]
        }
      ]
    }

    The client conversation

    When a feature request would break the budget — a full-page video background, a heavyweight slider library — we frame it as a trade-off, not a refusal. “We can ship this, but it will push LCP to ~3.8 s and likely drop your search ranking for these pages. Here’s a lighter alternative that achieves the same effect.” Clients almost always choose the lighter path when the cost is concrete.

    Performance budgets have saved us from scope creep disguised as features more than once. Build them in at the proposal stage and they become a shared language, not a developer complaint.