Skip to content

MPA Performance — Optimizing Server-Rendered Page Speed

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about MPA Performance. We cover key concepts, practical examples, and best practices to help you master this topic.

MPA performance optimization covers server response time, HTTP caching strategies, asset optimization, critical CSS inlining, image optimization, and Core Web Vitals improvement for server-rendered pages.

What You'll Learn

By the end of this tutorial, you will understand how to optimize MPA performance including server-side caching (page, fragment, database), asset optimization (CSS, JS, images), critical rendering path optimization, CDN configuration, and Core Web Vitals improvement strategies.

Why It Matters

Every second of load time costs 7 percent of conversions. For MPAs, each page load is a full request to the server, making performance optimization even more critical. Users expect pages to load in under 2 seconds. Poor performance directly impacts user satisfaction, SEO rankings, and business revenue.

Real-World Use

A news MPA reduced Time to First Byte from 1200ms to 200ms by implementing Redis page caching and Nginx reverse proxy caching. Combined with image optimization and critical CSS inlining, Largest Contentful Paint dropped from 4.5s to 1.2s. Monthly organic traffic increased 25 percent after Core Web Vitals improved from poor to good.

MPA Caching Architecture
    ┌──────────────────────────────────────────────────────────┐
    │                 MPA Caching Layers                        │
    ├──────────────────────────────────────────────────────────┤
    │                                                          │
    │  ┌──────────────────────────────────────────────────┐    │
    │  │  CDN Cache (CloudFlare, Fastly, CloudFront)      │    │
    │  │  Caches full pages at edge locations              │    │
    │  │  Cache duration: 5-60 minutes                     │    │
    │  └──────────────────────┬───────────────────────────┘    │
    │                         │                                 │
    │  ┌──────────────────────▼───────────────────────────┐    │
    │  │  Nginx / Varnish (Reverse Proxy Cache)           │    │
    │  │  Caches full pages behind the server             │    │
    │  │  Cache duration: 1-5 minutes                     │    │
    │  └──────────────────────┬───────────────────────────┘    │
    │                         │                                 │
    │  ┌──────────────────────▼───────────────────────────┐    │
    │  │  Application Cache (Redis, Memcached)            │    │
    │  │  Caches rendered HTML fragments                  │    │
    │  │  Cache duration: 30 seconds - 5 minutes          │    │
    │  └──────────────────────┬───────────────────────────┘    │
    │                         │                                 │
    │  ┌──────────────────────▼───────────────────────────┐    │
    │  │  Database Query Cache                            │    │
    │  │  Caches frequent query results                   │    │
    │  │  Invalidated on data change                      │    │
    │  └──────────────────────────────────────────────────┘    │
    │                                                          │
    └──────────────────────────────────────────────────────────┘

Think of MPA caching like a restaurant kitchen with multiple prep stations. The CDN is a freezer of pre-cooked meals. The Nginx cache is a warming drawer of recently cooked dishes. Redis is the prep station with pre-chopped ingredients. Database caching is the organized pantry. Each layer reduces the time to serve the next order.

Page Caching with Redis

const express = require('express');
const redis = require('redis');
const { promisify } = require('util');

const redisClient = redis.createClient();
const getAsync = promisify(redisClient.get).bind(redisClient);
const setAsync = promisify(redisClient.set).bind(redisClient);

// Page caching middleware
function pageCache(duration = 60) {
    return async (req, res, next) => {
        // Skip caching for authenticated users
        if (req.session && req.session.userId) {
            return next();
        }

        const cacheKey = `page:${req.originalUrl}`;

        try {
            const cached = await getAsync(cacheKey);
            if (cached) {
                console.log(`Cache HIT: ${req.originalUrl}`);
                return res.send(cached);
            }

            console.log(`Cache MISS: ${req.originalUrl}`);
            const originalSend = res.send.bind(res);
            res.send = async (body) => {
                await setAsync(cacheKey, body, 'EX', duration);
                originalSend(body);
            };

            next();
        } catch (error) {
            console.error('Cache error:', error);
            next();
        }
    };
}

// Apply caching to high-traffic pages
app.get('/', pageCache(300), homePage);
app.get('/products', pageCache(60), productListing);
app.get('/blog/:slug', pageCache(300), blogPost);

// Cache invalidation on content update
app.post('/admin/blog', async (req, res) => {
    // ... create blog post ...

    // Invalidate related caches
    await redisClient.del('page:/blog');
    await redisClient.keys('page:/blog/*').then(keys => {
        if (keys.length > 0) {
            redisClient.del(keys);
        }
    });

    res.redirect('/admin/blog');
});

Fragment Caching

<!-- EJS template with fragment caching -->
<%- include('partials/header') %>

<main>
    <h1>Product Listing</h1>

    <!-- Cache the product grid (expensive database query) -->
    <% if (cache.get('product_grid')) { %>
        <%- cache.get('product_grid') %>
    <% } else { %>
        <% const productHtml = ''; %>
        <% products.forEach(product => { %>
            <% productHtml += `
                <article>
                    <h2><a href="/products/${product.slug}">${product.name}</a></h2>
                    <p>$${product.price}</p>
                </article>
            `; %>
        <% }); %>
        <%- productHtml %>
        <% cache.set('product_grid', productHtml, 300); %>
    <% } %>

    <!-- Don't cache the personalized sidebar -->
    <%- include('partials/sidebar', { user: user }) %>
</main>

<%- include('partials/footer') %>

Asset Optimization

// Asset pipeline configuration (example with Gulp)
const gulp = require('gulp');
const cssnano = require('gulp-cssnano');
const terser = require('gulp-terser');
const imagemin = require('gulp-imagemin');
const webp = require('gulp-webp');
const rev = require('gulp-rev');
const revReplace = require('gulp-rev-replace');

// CSS optimization
gulp.task('styles', () => {
    return gulp.src('src/css/*.css')
        .pipe(cssnano())
        .pipe(rev())
        .pipe(gulp.dest('public/assets/css'))
        .pipe(rev.manifest('css-manifest.json'))
        .pipe(gulp.dest('public/assets'));
});

// JavaScript optimization
gulp.task('scripts', () => {
    return gulp.src('src/js/*.js')
        .pipe(terser({
            compress: { drop_console: true }
        }))
        .pipe(rev())
        .pipe(gulp.dest('public/assets/js'))
        .pipe(rev.manifest('js-manifest.json'))
        .pipe(gulp.dest('public/assets'));
});

// Image optimization
gulp.task('images', () => {
    return gulp.src('src/images/**/*.{jpg,png}')
        .pipe(imagemin([
            imagemin.mozjpeg({ quality: 80 }),
            imagemin.optipng({ optimizationLevel: 5 })
        ]))
        .pipe(webp({ quality: 80 }))
        .pipe(gulp.dest('public/assets/images'));
});

// Expected optimization results:
// CSS reduction: main.css 156KB -> main.min.css 28KB (82% reduction)
// JS reduction: app.js 45KB -> app.min.js 12KB (73% reduction)
// Image reduction: hero.jpg 240KB -> hero.webp 48KB (80% reduction)

Common Mistakes

  1. No caching strategy. Every uncached request hits the server, database, and renders templates. Without caching, a traffic spike can crash the server.
  2. Caching personalized content. Caching pages that contain user-specific data (cart, username) serves stale data. Only cache public pages or use ESI (Edge Side Includes) for personalized fragments.
  3. Not using a CDN. A CDN serves static assets and cached pages from locations close to the user. Without a CDN, every user connects to your origin server regardless of location.
  4. Blocking render with JavaScript. MPA pages should be usable without JavaScript. Defer non-critical scripts and load JavaScript asynchronously.
  5. Not optimizing images. Images are the largest assets on most pages. Use WebP format, serve responsive sizes with srcset, and lazy load below-fold images.

Practice Questions

  1. What are the four layers of caching in an MPA?
  2. How does page caching with Redis reduce server response time?
  3. What is fragment caching and when should you use it?
  4. How do you invalidate cache when content changes?
  5. Why should you use a CDN for MPA performance?

Challenge: Performance audit of an MPA using Lighthouse and WebPageTest. Implement: Redis page caching for the 5 most popular pages, fragment caching for product grid and sidebar, Nginx reverse proxy caching for static assets, image optimization (WebP, responsive sizes, Lazy Loading), critical CSS inlining, and CDN configuration. Measure before/after performance metrics.

FAQ

What is the ideal TTFB for an MPA?

Under 200ms. Good TTFB is 100-200ms. Over 500ms needs optimization. Use caching, CDN, and database query optimization to reduce TTFB.

Should I use full-page caching or fragment caching?

Full-page caching for anonymous users on public pages. Fragment caching for reusable components (navigation, sidebar) on pages with dynamic content.

How do I handle cache invalidation?

Invalidate by pattern: clear all pages related to changed content. Use Redis keys with patterns, cache tags, or a purging system on content publish.

Does HTTP/2 improve MPA performance?

Yes. HTTP/2 multiplexes multiple requests over one connection, reducing latency for loading multiple assets (CSS, JS, images) on each page.

What is the biggest MPA performance bottleneck?

Database queries on every request. Without caching, each page load triggers multiple queries. Implement query caching, N+1 query detection, and eager loading.

Mini Project

Optimize a blog MPA with 100+ posts for performance: Redis full-page caching for anonymous visitors (5-minute TTL), fragment caching for sidebar (recent posts, categories), image optimization (WebP, responsive srcset, lazy loading), critical CSS inlining for above-fold content, CDN configuration (Cloudflare or CloudFront), Nginx micro-caching, and before/after Lighthouse performance report.

What's Next

You understand MPA performance. Now explore Turbolinks and Hotwire to add SPA-like navigation to MPAs without JavaScript frameworks.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro