Web Application Performance Optimization: Core Web Vitals, Caching, CDN, and Database Tuning

Published 19 min read
performancecore-web-vitalscachingcdnpostgresqloptimization

Web application performance directly impacts revenue. Google reports that a 1-second delay in page load reduces conversions by 7%. A 3-second load time increases bounce rate by 32% compared to a 1-second load time. This guide covers five performance layers: Core Web Vitals, browser caching, server-side caching, CDN strategy, and database query optimization. Each section includes measurable targets and tested configurations.

According to HTTP Archive data from March 2026, the median page weight for web applications is 2.4 MB with a Largest Contentful Paint (LCP) of 3.8 seconds on mobile. The top 10% of performing sites achieve LCP under 1.5 seconds. The difference is not hardware or bandwidth but architectural decisions that compound across every page load.

---

Core Web Vitals Targets

MetricGoodNeeds ImprovementPoor

LCP (Largest Contentful Paint)< 2.5s2.5s - 4.0s> 4.0s
INP (Interaction to Next Paint)< 200ms200ms - 500ms> 500ms
CLS (Cumulative Layout Shift)< 0.10.1 - 0.25> 0.25

LCP Optimization Checklist

  • Preload the LCP image: Add `` in the ``
  • Use `loading="eager"` on the LCP image: Only the hero/LCP image. All other images use `loading="lazy"`
  • Serve images in AVIF/WebP: AVIF saves 50% over JPEG, WebP saves 30%
  • Inline critical CSS: Extract above-the-fold CSS and inline it in the HTML
  • Eliminate render-blocking resources: Defer non-critical JS, async load analytics
  • <!-- Optimal LCP image loading -->
    

    <head>

    <link rel="preload" as="image" href="/hero.avif" type="image/avif">

    <style>/* Critical CSS inlined here */</style>

    </head>

    <body>

    <picture>

    <source srcset="/hero.avif" type="image/avif">

    <source srcset="/hero.webp" type="image/webp">

    <img src="/hero.jpg" alt="Hero image"

    loading="eager" fetchpriority="high"

    width="1200" height="628">

    </picture>

    </body>

    INP Optimization

    Interaction to Next Paint measures how quickly the page responds to user input. The primary causes of poor INP are long JavaScript tasks blocking the main thread.

    • Break long tasks into smaller chunks using `requestIdleCallback` or `scheduler.yield()`
    • Defer third-party scripts (analytics, chat widgets, tracking pixels)
    • Use web workers for CPU-intensive computations
    • Virtualize long lists (react-window or @tanstack/virtual)
    • Debounce rapid-fire events (scroll, resize, input)

    ---

    Multi-Layer Caching Strategy

    Effective caching operates at four layers, each reducing load on the layer below it.

    LayerCache TypeTTLHit Rate Target

    BrowserHTTP cache headers1 year (static), 0 (API)90%+ for static
    CDNEdge cache1 hour - 1 day80%+ for static
    ApplicationRedis/Memcached5 min - 1 hour70%+ for read-heavy
    DatabaseQuery result cache1 min - 15 min50%+

    HTTP Cache Headers

    <h1>nginx cache configuration</h1>
    

    server {

    # Static assets: cache for 1 year (use content hashing in filenames)

    location ~* \.(js

    csspngjpgjpeggificosvgwoff2avif
    webp)$ {

    expires 365d;

    add_header Cache-Control "public, immutable";

    }

    # HTML: no cache (always fetch fresh)

    location ~* \.html$ {

    add_header Cache-Control "no-cache, no-store, must-revalidate";

    }

    # API responses: short cache for read endpoints

    location /api/ {

    add_header Cache-Control "private, max-age=0, must-revalidate";

    }

    }

    Application-Level Caching with Redis

    <h1>Python/FastAPI caching pattern</h1>
    

    import redis

    import json

    from functools import wraps

    redis_client = redis.Redis(host="localhost", port=6379, db=0)

    def cache_response(ttl_seconds=300):

    def decorator(func):

    @wraps(func)

    async def wrapper(*args, **kwargs):

    cache_key = f"{func.__name__}:{hash(str(args) + str(kwargs))}"

    cached = redis_client.get(cache_key)

    if cached:

    return json.loads(cached)

    result = await func(*args, **kwargs)

    redis_client.setex(cache_key, ttl_seconds, json.dumps(result))

    return result

    return wrapper

    return decorator

    @cache_response(ttl_seconds=300)

    async def get_dashboard_metrics(user_id: int):

    # Expensive database query cached for 5 minutes

    return await db.fetch_dashboard(user_id)

    ---

    CDN Configuration

    A CDN reduces latency by serving content from edge locations near users. For applications hosted on Arxentra, integrate a CDN for static assets while keeping API calls direct to the origin. See Services Overview for Arxentra CDN options.

    CDN Best Practices

    • Cache static assets at the edge with long TTLs
    • Use cache-busting via content hashes in filenames (e.g., `app.a1b2c3.js`)
    • Set up a custom domain on the CDN to avoid CORS issues
    • Enable Brotli compression at the edge for 15-20% smaller payloads than gzip
    • Configure cache purge webhooks in your CI/CD pipeline

    ---

    Database Query Optimization

    Database queries are the most common performance bottleneck in web applications. The 80/20 rule applies: 80% of query time comes from 20% of queries.

    -- Find the slowest queries in PostgreSQL
    

    SELECT query, calls, mean_exec_time, total_exec_time

    FROM pg_stat_statements

    ORDER BY mean_exec_time DESC

    LIMIT 20;

    -- Check for missing indexes

    SELECT relname, seq_scan, seq_tup_read, idx_scan, idx_tup_fetch

    FROM pg_stat_user_tables

    WHERE seq_scan > 1000

    ORDER BY seq_tup_read DESC;

    -- Create targeted indexes based on slow query analysis

    CREATE INDEX CONCURRENTLY idx_services_user_status

    ON services (user_id, status)

    WHERE status IN ('running', 'pending');

    Query Optimization Rules

  • Always use EXPLAIN ANALYZE before optimizing - measure, do not guess
  • Add indexes for WHERE, JOIN, and ORDER BY columns used in slow queries
  • Use partial indexes for columns with low cardinality filtered conditions
  • Avoid SELECT *: select only needed columns to reduce I/O
  • Use connection pooling (PgBouncer) to prevent connection exhaustion
  • Set statement_timeout to prevent runaway queries: `SET statement_timeout = '5s';`
  • For PostgreSQL hosting with optimized defaults, see Pricing for managed database plans that include automated query analysis.

    ---

    Load Testing Baseline

    Every performance optimization should be validated with load testing. Use k6 for scriptable, developer-friendly load tests.

    // k6-load-test.js
    

    import http from "k6/http";

    import { check, sleep } from "k6";

    export const options = {

    stages: [

    { duration: "2m", target: 50 }, // ramp up

    { duration: "5m", target: 50 }, // sustained load

    { duration: "2m", target: 100 }, // peak

    { duration: "5m", target: 100 }, // sustained peak

    { duration: "2m", target: 0 }, // ramp down

    ],

    thresholds: {

    http_req_duration: ["p(95)<500"], // 95th percentile under 500ms

    http_req_failed: ["rate<0.01"], // less than 1% errors

    },

    };

    export default function () {

    const res = http.get("https://your-app.arxentra.cloud/api/health");

    check(res, { "status is 200": (r) => r.status === 200 });

    sleep(1);

    }

    ---

    Frequently Asked Questions

    How do I measure Core Web Vitals in production?

    Use the web-vitals JavaScript library to capture real user metrics (RUM) and send them to your analytics. Google Search Console also reports field data for pages with sufficient traffic. Lighthouse provides lab data for development.

    What is the most impactful single performance optimization?

    Adding proper HTTP cache headers to static assets. It requires zero code changes, takes 10 minutes to configure in nginx, and eliminates redundant downloads for returning users. This alone can reduce page load time by 40-60% for repeat visitors.

    How much does a CDN actually improve performance?

    For a globally distributed audience, a CDN reduces Time to First Byte (TTFB) by 50-80% for static assets. For a US-only audience with a US-based origin server, the improvement is 20-40%. The benefit increases with geographic distance between users and origin.

    Should I use Redis or Memcached for application caching?

    Redis for most use cases. It supports data structures beyond key-value (lists, sets, sorted sets), persistence, and pub/sub. Memcached is faster for simple key-value caching but lacks Redis's versatility. Redis uses approximately 30% more memory per key.

    How often should I run load tests?

    Run load tests before every major release and after any infrastructure change. Integrate a lightweight smoke load test (30 seconds, 10 virtual users) into your CI/CD pipeline. Run full load tests (15 minutes, production-level traffic) weekly or bi-weekly.

    What database connection pool size should I use?

    Start with `pool_size = (CPU cores * 2) + effective_spindle_count`. For an Arxentra VPS with 4 cores and NVMe storage (effective_spindle_count of 1 for SSD/NVMe), that is 4 x 2 + 1 = 9 connections. Monitor `pg_stat_activity` for connection utilization. Most web applications need 10-25 connections; exceeding 100 usually indicates a connection leak. See Documentation for connection pooling configuration guides.