A few years ago, I found myself working on one of the most demanding yet exciting projects of my career—an MVP for an online food ordering system. We were tasked with building a service that optimized performance for high-traffic installs. The system had to handle both static and dynamic content: static food options and dynamic content related to each visitor’s cart and checkout process.
The challenge? We couldn’t use a Content Delivery Network (CDN) to speed up the delivery of assets like images, CSS, or JavaScript files. Every piece of content had to be served efficiently from our own infrastructure. Building a solution that balanced speed and maintainability while caching large assets was critical.
Here’s how we tackled it.
Stage 1: Optimizing HTML and Asset Delivery without a CDN
The first thing we needed to address was caching. Since we couldn’t rely on a CDN, we had to build our own system for caching not just the HTML content but also every asset embedded in the pages—JavaScript, CSS, and images. This would minimize redundant requests, which was key to handling the expected traffic.
We designed a custom caching layer in PHP that:
- Cached the HTML content of pages.
- Parsed the HTML to find and cache all associated assets (e.g., images, JS, CSS).
- Served cached versions of these assets on subsequent requests, drastically reducing server load.
Here’s a more complex example of how we achieved this:
<?php
class AssetCache {
private $cacheDir = '/tmp/cache/'; // Directory for cached assets
// Function to cache HTML content along with assets (JS, CSS, images)
public function cachePageAndAssets($url) {
// Fetch and cache the HTML content
$htmlContent = $this->fetchContent($url);
$this->cacheContent($url, $htmlContent);
// Parse the HTML to find asset URLs (JS, CSS, images)
$assetUrls = $this->extractAssetUrls($htmlContent);
// Cache each asset
foreach ($assetUrls as $assetUrl) {
$assetContent = $this->fetchContent($assetUrl);
$this->cacheContent($assetUrl, $assetContent);
}
}
// Function to serve the cached page or assets
public function serveFromCache($url) {
$cacheFile = $this->getCacheFilePath($url);
if (file_exists($cacheFile)) {
return file_get_contents($cacheFile); // Serve from cache
} else {
return false; // Cache miss
}
}
// Fetch content from the source (HTML page or assets)
private function fetchContent($url) {
return file_get_contents($url); // Basic implementation, can be expanded for error handling
}
// Cache content (HTML or asset)
private function cacheContent($url, $content) {
$cacheFile = $this->getCacheFilePath($url);
file_put_contents($cacheFile, $content); // Save to cache
}
// Get the cache file path for a given URL
private function getCacheFilePath($url) {
return $this->cacheDir . md5($url) . '.cache';
}
// Extract asset URLs from HTML content (JS, CSS, images)
private function extractAssetUrls($htmlContent) {
$assetUrls = [];
// Extract all script, link, and img tags from the HTML
$doc = new DOMDocument();
@$doc->loadHTML($htmlContent);
// Extract JS files
foreach ($doc->getElementsByTagName('script') as $script) {
if ($script->getAttribute('src')) {
$assetUrls[] = $script->getAttribute('src');
}
}
// Extract CSS files
foreach ($doc->getElementsByTagName('link') as $link) {
if ($link->getAttribute('rel') === 'stylesheet') {
$assetUrls[] = $link->getAttribute('href');
}
}
// Extract image files
foreach ($doc->getElementsByTagName('img') as $img) {
if ($img->getAttribute('src')) {
$assetUrls[] = $img->getAttribute('src');
}
}
return $assetUrls;
}
}
// Example usage:
$pageUrl = "https://order....co.uk/menu";
// Create an instance of the caching system
$assetCache = new AssetCache();
// Try serving from cache
$cachedPage = $assetCache->serveFromCache($pageUrl);
if ($cachedPage) {
echo "Served from cache: " . $cachedPage;
} else {
// Cache the page and its assets for future requests
$assetCache->cachePageAndAssets($pageUrl);
echo "Page and assets cached!";
}
?>
In the above example, the system:
- Fetches the HTML content for a page.
- Parses that HTML to find associated assets such as JavaScript, CSS, and images.
- Caches both the HTML content and the assets so that future requests for the same page can be served directly from the cache.
This caching mechanism drastically reduced the number of redundant requests and improved load times. For each visit, we minimized the server’s workload by serving as much as possible from the local cache.
Stage 2: Handling Dynamic Content with Efficiency
While caching the static content and assets solved part of the problem, we still had to manage the dynamic aspects of the system, such as the cart and checkout process. Each visitor interacted with these parts differently, so caching dynamic content was out of the question.
We took a hybrid approach:
- Cache the static parts of the page (the food options and layout).
- Use AJAX requests to fetch and update the dynamic sections of the page, such as the cart.
By separating static and dynamic content, we maintained a fast user experience for the static parts of the page, while keeping the dynamic parts responsive to user actions. Here’s how we handled the cart dynamically:
<?php
class CartManager {
private $cart = [];
// Add an item to the cart
public function addToCart($itemId, $quantity) {
if (isset($this->cart[$itemId])) {
$this->cart[$itemId] += $quantity; // Update quantity if item exists
} else {
$this->cart[$itemId] = $quantity; // Add new item to the cart
}
}
// Remove an item from the cart
public function removeFromCart($itemId) {
if (isset($this->cart[$itemId])) {
unset($this->cart[$itemId]); // Remove item from cart
}
}
// Get the cart content
public function getCart() {
return $this->cart;
}
// Process the checkout (basic implementation)
public function checkout() {
// Simulate checkout process (e.g., store cart data in database)
// In a real-world scenario, this would interact with payment and order systems
return "Checkout completed for " . count($this->cart) . " items.";
}
}
// Example AJAX call to manage the cart dynamically
$cartManager = new CartManager();
$cartManager->addToCart(101, 2); // Add two units of item 101
$cartManager->addToCart(102, 1); // Add one unit of item 102
echo json_encode($cartManager->getCart()); // Return the updated cart as JSON
By using AJAX, we were able to load dynamic elements like the cart separately from the main page content. This allowed the static parts of the page to be served from cache while ensuring that the dynamic content remained interactive and responsive.
Stage 3: Iteration and Testing Under Real Traffic
Once the MVP was up and running, the final phase was stress-testing the system under real-world conditions. With the caching layer in place and the dynamic parts isolated, we could handle high traffic without significant slowdowns. As we gathered data from live users, we continuously iterated on the system—tweaking the caching logic, optimizing asset delivery, and refining how we handled dynamic content.
MVP as the Foundation for a Full Product
The MVP wasn’t the final product, but it became the stable foundation for a new service. This service ended up being used by many online food ordering websites across multiple domains. By building smart from the start, we were able to scale the MVP into a production-ready solution, avoiding the technical debt that often comes with rushed prototypes.
Conclusion: Focus on Building a Robust MVP, Not a Fancy One
The biggest lesson I took from this project is that an MVP isn’t about being fancy—it’s about proving the value it can bring. Experiment with different ideas, understand the requirements, and build something robust that will serve as a solid foundation for future development. In this case, our caching and dynamic content handling approach provided that foundation, allowing the service to scale as traffic grew.
If you’re in the process of building an MVP, make sure it works, scales, and proves its value. The bells and whistles can come later—focus on creating something that delivers results from day one.