Back to Blog
Web Development 4 min read

How I Optimized Page Load from 3.8s to 1.4s on Rural 4G Networks

A deep dive into the performance optimization strategies I used at SALUT Universitas Terbuka to make our web properties usable on slow, unstable Indonesian rural networks.

SA
Hermanto Steven
Performance optimization visualization showing speed improvements

When I joined SALUT Universitas Terbuka Tana Toraja in 2019, our main website had a median load time of 3.8 seconds on 4G connections. For users in rural South Sulawesi, where network speeds often drop to 3G or worse, the experience was frustrating—if the page loaded at all.

Fast forward to today, and we’ve brought that number down to 1.4 seconds. Here’s exactly how we did it.

The Problem: Real-World Network Conditions

Before diving into solutions, let me paint the picture. Our users include:

  • Students accessing course materials from mountain villages
  • Administrative staff on shared office connections
  • Prospective students browsing on mobile data

The common thread? Unreliable, slow connections. A Tokyo-optimized CDN doesn’t help when the last mile is a congested cell tower serving an entire district.

Strategy 1: Aggressive HTML Caching

The biggest win came from caching entire HTML responses. Here’s our approach:

// Simplified cache wrapper
function getCachedPage($key, $ttl, $callback) {
    $cached = Cache::get($key);
    if ($cached) return $cached;
    
    $html = $callback();
    Cache::put($key, $html, $ttl);
    return $html;
}

// Usage in controller
$html = getCachedPage(
    "home_page_" . app()->getLocale(),
    3600, // 1 hour
    fn() => view('home')->render()
);

Results: Page generation dropped from ~800ms to ~15ms for cached hits.

Cache Invalidation Strategy

We use a simple event-based approach:

  1. Content update triggers cache key deletion
  2. First visitor regenerates the cache
  3. Background job pre-warms critical pages after deployments

Strategy 2: Asset Budgets

We set hard limits on what we ship:

Asset TypeBudgetActual
Total JS100KB87KB
Total CSS50KB42KB
Hero Image150KB120KB
Total Page400KB340KB

How We Enforce Budgets

// In our build script
const budgets = {
  js: 100 * 1024,
  css: 50 * 1024,
  total: 400 * 1024
};

// Fail the build if exceeded
if (stats.js > budgets.js) {
  throw new Error(`JS budget exceeded: ${stats.js} > ${budgets.js}`);
}

Strategy 3: Image Pipeline

Images were our biggest payload. Our solution:

  1. Server-side resize on upload (max 1200px wide)
  2. WebP conversion with JPEG fallback
  3. Lazy loading for below-fold images
  4. Blur placeholder for perceived performance
// Image processing on upload
public function processImage($file) {
    $image = Image::make($file);
    
    // Resize if too large
    if ($image->width() > 1200) {
        $image->resize(1200, null, fn($c) => $c->aspectRatio());
    }
    
    // Save WebP version
    $image->encode('webp', 80)->save($webpPath);
    
    // Generate blur placeholder
    $placeholder = $image->blur(50)->resize(20, null)->encode('base64');
    
    return [
        'webp' => $webpPath,
        'placeholder' => $placeholder
    ];
}

Strategy 4: Database Query Optimization

Our dashboard was making 47 queries on the home page. We brought it down to 8.

Before: N+1 Hell

// Bad: N+1 queries
$posts = Post::all();
foreach ($posts as $post) {
    echo $post->author->name; // Query per post!
}

After: Eager Loading

// Good: 2 queries total
$posts = Post::with(['author', 'category'])->get();

We also added composite indexes for common query patterns:

ALTER TABLE posts ADD INDEX idx_published_date (status, published_at);
ALTER TABLE users ADD INDEX idx_role_active (role, is_active);

Strategy 5: Graceful Degradation

Not everything needs to work perfectly on slow connections. We prioritize:

  1. Core content loads first (text, essential images)
  2. Enhancements load progressively (animations, 3D elements)
  3. Non-essential features are skipped entirely on slow connections
// Detect connection quality
const connection = navigator.connection;

if (connection?.effectiveType === '4g') {
  loadFancyAnimations();
} else {
  // Skip heavy features on slow connections
  document.body.classList.add('reduced-motion');
}

Results Summary

MetricBeforeAfterImprovement
Median Load Time3.8s1.4s63% faster
Time to First Byte1.2s0.18s85% faster
Total Page Size1.8MB340KB81% smaller
Lighthouse Score4291+49 points

Key Takeaways

  1. Measure on real networks, not just localhost or fast office WiFi
  2. Cache aggressively at the HTML level, not just assets
  3. Set and enforce budgets before optimization becomes necessary
  4. Graceful degradation is better than broken features
  5. Profile your database queries—they’re often the hidden bottleneck

The best performance optimization is the one that makes your slowest users happy. For us, that means thinking about the student in a Toraja village trying to check their grades on a weak signal.


Have questions about performance optimization for emerging markets? Get in touch.

#Performance #PHP #Caching #Optimization #MySQL

Share this article: