Back to all articles
Case Study2025-01-15· 11 min read

How We Cut Page Load Time by 65%: Modernising a Legacy E-Commerce Platform

A detailed case study on migrating a decade-old PHP monolith to .NET 8 and Nuxt 3—delivering a 28% conversion rate increase, 47% more mobile orders, and monthly releases replaced by daily deploys.

.NETVue.jsE-CommerceModernisationCase Study
How We Cut Page Load Time by 65%: Modernising a Legacy E-Commerce Platform

The Business Problem

FreshMarket, a growing grocery delivery service, came to MediaFront with a platform that had become a business liability. Built ten years earlier as a PHP/jQuery monolith, the codebase had accumulated so much technical debt that even simple features took weeks to ship—and those features frequently broke in production.

The impact was measurable:

  • 5.2-second page load times driving mobile users to competitors
  • System crashes during promotional events — the platform would buckle at 3× normal traffic
  • 62% cart abandonment rate — far above the 50% industry average
  • Monthly release cycles with a 40% regression rate

The brief was clear: rebuild for performance, scalability, and velocity—without taking the platform offline for months.

The Strategy: Strangle, Don't Rewrite

The fastest way to deliver value without risk is the strangler fig pattern: build a new system incrementally alongside the old one, routing traffic to the new components as they mature, until nothing is left to strangle.

We rejected a full rewrite for two reasons. First, big-bang rewrites routinely fail or deliver years late. Second, FreshMarket couldn't stop selling groceries while we worked.

Our phased approach over eight months:

  1. Month 1–2: Build the API layer and CI/CD infrastructure
  2. Month 3–4: Rebuild the product catalogue, search, and checkout in Nuxt 3
  3. Month 5–6: Migrate order processing and inventory to .NET 8 microservices
  4. Month 7–8: Decommission legacy PHP, performance optimisation, launch

An Azure API Management gateway sat in front of both systems throughout, routing based on URL path. Users never saw the migration happening.

The Technical Architecture

Backend: .NET 8 with Clean Architecture

We built the backend on .NET 8 using a clean architecture approach—domain logic isolated from infrastructure concerns, with CQRS (Command Query Responsibility Segregation) separating reads from writes at the service boundary.

// Command: write path — validated, transactional, produces domain events
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Result<int>>
{
    private readonly IOrderRepository _orders;
    private readonly IEventBus _events;

    public async Task<Result<int>> Handle(
        CreateOrderCommand cmd,
        CancellationToken ct)
    {
        var order = Order.Create(cmd.CustomerId, cmd.Items);

        if (order.IsFailure)
            return Result.Failure<int>(order.Error);

        await _orders.SaveAsync(order.Value, ct);
        await _events.PublishAsync(new OrderCreatedEvent(order.Value.Id), ct);

        return Result.Success(order.Value.Id);
    }
}

// Query: read path — optimised for speed, no domain model overhead
public class GetOrderSummaryQueryHandler : IRequestHandler<GetOrderSummaryQuery, OrderSummaryDto>
{
    private readonly IDbConnection _db;

    public async Task<OrderSummaryDto> Handle(GetOrderSummaryQuery query, CancellationToken ct)
    {
        // Direct Dapper query against denormalised read model — sub-10ms
        return await _db.QuerySingleAsync<OrderSummaryDto>(
            "SELECT * FROM v_order_summaries WHERE id = @Id",
            new { query.Id });
    }
}

Key decisions that paid off: MediatR for decoupled command/query dispatch, Azure Service Bus for asynchronous cross-service communication, and Redis for hot product catalogue data with a 94% cache hit rate in production.

Frontend: Nuxt 3 with Server-Side Rendering

The customer-facing frontend was rebuilt in Nuxt 3 with SSR enabled for all product and checkout pages — critical for both SEO and perceived performance on slow mobile connections.

// Nuxt 3 composable: type-safe product fetching with caching
const useProduct = (id: string) => {
  return useAsyncData(
    `product-${id}`,
    () => $fetch<Product>(`/api/products/${id}`),
    {
      // Cache in Nuxt's built-in data layer — no duplicate requests
      dedupe: 'defer',
      transform: (data) => ({
        ...data,
        // Pre-format price on server to avoid hydration mismatch
        formattedPrice: new Intl.NumberFormat('nl-NL', {
          style: 'currency',
          currency: 'EUR',
        }).format(data.price),
      }),
    }
  );
};

We implemented Tailwind CSS for consistent utility-first styling, Pinia for client-side state (cart, wishlist, user session), and lazy-loaded non-critical components to keep initial JS bundles under 120kB.

Measuring What Matters

We tracked three categories of metrics from day one:

Performance (Lighthouse, Real User Monitoring)

MetricBeforeAfterChange
Page load time (median)5.2s1.8s−65%
Server response time850ms120ms−86%
Lighthouse Performance4289+112%
Core Web Vitals (LCP)6.1s1.9s✅ Good

Business Metrics (3 months post-launch)

MetricBeforeAfterChange
Conversion ratebaseline+28%📈
Mobile ordersbaseline+47%📈
Cart abandonment62%41%−34%
Average order valuebaseline+12%📈

Engineering Velocity

MetricBeforeAfter
Deploy frequencymonthly3–4× per week
Deploy-related incidentsbaseline−92%
Mean time to recoveryhoursminutes
Onboarding time (new dev)~3 weeks~4 days

The Lessons That Carried Forward

Start where the customer feels the pain. We rebuilt the checkout flow in month three, before the product catalogue was even finished on the new stack. A 28% conversion lift started accumulating six months before the migration completed.

Invest in observability from week one. We instrumented every API endpoint with Azure Application Insights on day one. Without real latency data, we would have optimised the wrong things. With it, we identified that 70% of total response time came from two database queries — both fixable.

Strangler is slower to start, faster to finish. The first two months felt slow — no visible user-facing changes. But by month four, we were shipping multiple features per week to a progressively growing slice of real production traffic, validating changes with real users rather than staging assumptions.

The modernised FreshMarket platform now handles 5× the peak traffic of the old system, on 30% less infrastructure spend. More importantly, the team ships with confidence — because the architecture was built to evolve.

Want to work together?

We build high-performance web applications and backend systems.

Get in touch