{"id":3577,"date":"2025-12-28T15:44:17","date_gmt":"2025-12-28T12:44:17","guid":{"rendered":"https:\/\/www.dchost.com\/blog\/http-cache-control-etag-and-cdn-rules-for-faster-wordpress-laravel-and-spa-sites\/"},"modified":"2025-12-28T15:44:17","modified_gmt":"2025-12-28T12:44:17","slug":"http-cache-control-etag-and-cdn-rules-for-faster-wordpress-laravel-and-spa-sites","status":"publish","type":"post","link":"https:\/\/www.dchost.com\/blog\/en\/http-cache-control-etag-and-cdn-rules-for-faster-wordpress-laravel-and-spa-sites\/","title":{"rendered":"HTTP Cache-Control, ETag and CDN Rules for Faster WordPress, Laravel and SPA Sites"},"content":{"rendered":"<div class=\"dchost-blog-content-wrapper\"><p>Page speed problems on WordPress, Laravel or modern SPA frontends are often blamed on PHP, the database or the hosting plan. In reality, a surprising amount of \u201cslowness\u201d comes from missing or incorrect browser caching. Every time a visitor reloads your site, their browser must decide: \u201cCan I reuse what I already have, or do I need to hit the server again?\u201d That decision is entirely driven by HTTP caching headers and, if you use one, your CDN\u2019s cache rules.<\/p>\n<p>In this article we\u2019ll look at how to correctly use <strong>Cache-Control<\/strong>, <strong>ETag<\/strong> and CDN cache rules for real-world WordPress, Laravel and SPA (React, Vue, Angular) projects. We\u2019ll keep the focus practical: which files should get long-lived caching, which responses should stay dynamic, and how you avoid the classic \u201cI updated CSS but users still see the old version\u201d problem. At dchost.com we tune these settings daily on shared hosting, <a href=\"https:\/\/www.dchost.com\/vps\">VPS<\/a> and <a href=\"https:\/\/www.dchost.com\/dedicated-server\">dedicated server<\/a>s, so the examples below reflect what actually works in production, not just what the RFC says.<\/p>\n<div id=\"toc_container\" class=\"toc_transparent no_bullets\"><p class=\"toc_title\">\u0130&ccedil;indekiler<\/p><ul class=\"toc_list\"><li><a href=\"#Why_HTTP_Caching_Matters_So_Much_for_Modern_Sites\"><span class=\"toc_number toc_depth_1\">1<\/span> Why HTTP Caching Matters So Much for Modern Sites<\/a><\/li><li><a href=\"#Core_HTTP_Caching_Concepts_You_Really_Need\"><span class=\"toc_number toc_depth_1\">2<\/span> Core HTTP Caching Concepts You Really Need<\/a><ul><li><a href=\"#Cache-Control_the_main_steering_wheel\"><span class=\"toc_number toc_depth_2\">2.1<\/span> Cache-Control: the main steering wheel<\/a><\/li><li><a href=\"#ETag_and_conditional_requests\"><span class=\"toc_number toc_depth_2\">2.2<\/span> ETag and conditional requests<\/a><\/li><li><a href=\"#Last-Modified_and_304_responses\"><span class=\"toc_number toc_depth_2\">2.3<\/span> Last-Modified and 304 responses<\/a><\/li><\/ul><\/li><li><a href=\"#Browser_Caching_Strategy_for_WordPress\"><span class=\"toc_number toc_depth_1\">3<\/span> Browser Caching Strategy for WordPress<\/a><ul><li><a href=\"#Static_assets_CSS_JS_images_fonts\"><span class=\"toc_number toc_depth_2\">3.1<\/span> Static assets: CSS, JS, images, fonts<\/a><\/li><li><a href=\"#Dynamic_HTML_posts_pages_and_WooCommerce\"><span class=\"toc_number toc_depth_2\">3.2<\/span> Dynamic HTML: posts, pages and WooCommerce<\/a><\/li><li><a href=\"#Example_safe_HTML_caching_for_anonymous_visitors\"><span class=\"toc_number toc_depth_2\">3.3<\/span> Example: safe HTML caching for anonymous visitors<\/a><\/li><\/ul><\/li><li><a href=\"#Browser_Caching_Strategy_for_Laravel\"><span class=\"toc_number toc_depth_1\">4<\/span> Browser Caching Strategy for Laravel<\/a><ul><li><a href=\"#Static_assets_let_ViteMix_and_Nginx_do_the_heavy_lifting\"><span class=\"toc_number toc_depth_2\">4.1<\/span> Static assets: let Vite\/Mix and Nginx do the heavy lifting<\/a><\/li><li><a href=\"#Blade_HTML_conservative_but_smart\"><span class=\"toc_number toc_depth_2\">4.2<\/span> Blade HTML: conservative, but smart<\/a><\/li><li><a href=\"#Laravel_JSON_APIs_when_to_cache_and_when_not_to\"><span class=\"toc_number toc_depth_2\">4.3<\/span> Laravel JSON APIs: when to cache and when not to<\/a><\/li><\/ul><\/li><li><a href=\"#Browser_Caching_Strategy_for_SPA_Frontends_React_Vue_Angular\"><span class=\"toc_number toc_depth_1\">5<\/span> Browser Caching Strategy for SPA Frontends (React, Vue, Angular)<\/a><ul><li><a href=\"#indexhtml_treat_it_like_dynamic_HTML\"><span class=\"toc_number toc_depth_2\">5.1<\/span> index.html: treat it like dynamic HTML<\/a><\/li><li><a href=\"#JSCSS_bundles_and_assets_immutable_versioned_long-lived\"><span class=\"toc_number toc_depth_2\">5.2<\/span> JS\/CSS bundles and assets: immutable, versioned, long-lived<\/a><\/li><li><a href=\"#Service_workers_powerful_but_not_a_magic_bullet\"><span class=\"toc_number toc_depth_2\">5.3<\/span> Service workers: powerful, but not a magic bullet<\/a><\/li><\/ul><\/li><li><a href=\"#How_CDN_Cache_Rules_Interact_with_Browser_Cache\"><span class=\"toc_number toc_depth_1\">6<\/span> How CDN Cache Rules Interact with Browser Cache<\/a><ul><li><a href=\"#Origin_vs_edge_who_is_in_charge\"><span class=\"toc_number toc_depth_2\">6.1<\/span> Origin vs edge: who is in charge?<\/a><\/li><li><a href=\"#Essential_CDN_cache_rules_for_WordPress\"><span class=\"toc_number toc_depth_2\">6.2<\/span> Essential CDN cache rules for WordPress<\/a><\/li><li><a href=\"#CDN_rules_for_Laravel_and_APIs\"><span class=\"toc_number toc_depth_2\">6.3<\/span> CDN rules for Laravel and APIs<\/a><\/li><li><a href=\"#CDN_rules_for_SPA_frontends\"><span class=\"toc_number toc_depth_2\">6.4<\/span> CDN rules for SPA frontends<\/a><\/li><\/ul><\/li><li><a href=\"#Practical_Checklist_and_Common_Gotchas\"><span class=\"toc_number toc_depth_1\">7<\/span> Practical Checklist and Common Gotchas<\/a><ul><li><a href=\"#1_Separate_assets_from_HTML_clearly\"><span class=\"toc_number toc_depth_2\">7.1<\/span> 1. Separate assets from HTML clearly<\/a><\/li><li><a href=\"#2_Implement_asset_fingerprinting\"><span class=\"toc_number toc_depth_2\">7.2<\/span> 2. Implement asset fingerprinting<\/a><\/li><li><a href=\"#3_Decide_your_HTML_strategy\"><span class=\"toc_number toc_depth_2\">7.3<\/span> 3. Decide your HTML strategy<\/a><\/li><li><a href=\"#4_Configure_CDN_cache_keys_and_exclusions\"><span class=\"toc_number toc_depth_2\">7.4<\/span> 4. Configure CDN cache keys and exclusions<\/a><\/li><li><a href=\"#5_Test_with_DevTools_and_curl\"><span class=\"toc_number toc_depth_2\">7.5<\/span> 5. Test with DevTools and curl<\/a><\/li><li><a href=\"#6_Monitor_real-world_impact\"><span class=\"toc_number toc_depth_2\">7.6<\/span> 6. Monitor real-world impact<\/a><\/li><\/ul><\/li><li><a href=\"#Bringing_It_All_Together_and_What_to_Do_Next\"><span class=\"toc_number toc_depth_1\">8<\/span> Bringing It All Together (and What to Do Next)<\/a><\/li><\/ul><\/div>\n<h2><span id=\"Why_HTTP_Caching_Matters_So_Much_for_Modern_Sites\">Why HTTP Caching Matters So Much for Modern Sites<\/span><\/h2>\n<p>HTTP caching is the cheapest performance optimization you can deploy:<\/p>\n<ul>\n<li><strong>Browsers load faster<\/strong> because static assets (CSS, JS, images, fonts) are reused locally instead of fetched on every page view.<\/li>\n<li><strong>Your origin server handles fewer requests<\/strong>, leaving more CPU and RAM for real dynamic work (search, checkout, dashboards).<\/li>\n<li><strong>CDNs become dramatically more effective<\/strong> because they can keep assets at the edge longer, improving global latency and reducing bandwidth bills.<\/li>\n<\/ul>\n<p>The good news: for most WordPress, Laravel and SPA projects, you don\u2019t need complex logic. You mainly need three layers done right:<\/p>\n<ol>\n<li>Correct <strong>Cache-Control<\/strong> (and sometimes <strong>Expires<\/strong>) headers on your origin.<\/li>\n<li>Sensible <strong>ETag<\/strong> \/ <strong>Last-Modified<\/strong> behavior for conditional requests.<\/li>\n<li>CDN cache rules that respect your origin headers but override them where necessary (especially for HTML and APIs).<\/li>\n<\/ol>\n<p>If you want a deeper conceptual dive specifically into headers like <code>immutable<\/code>, ETag vs Last-Modified and fingerprinting, we also have a dedicated article: <a href=\"https:\/\/www.dchost.com\/blog\/en\/nereden-baslamaliyiz-bir-css-dosyasinin-pesinde\/\">Stop Fighting Your Cache: Cache-Control immutable, ETag vs Last\u2011Modified, and Asset Fingerprinting<\/a>. Here we\u2019ll stay focused on practical recipes per framework and how to combine them with a CDN.<\/p>\n<h2><span id=\"Core_HTTP_Caching_Concepts_You_Really_Need\">Core HTTP Caching Concepts You Really Need<\/span><\/h2>\n<h3><span id=\"Cache-Control_the_main_steering_wheel\">Cache-Control: the main steering wheel<\/span><\/h3>\n<p><strong>Cache-Control<\/strong> is the primary HTTP\/1.1 header used by browsers and CDNs to decide <em>whether<\/em> and <em>for how long<\/em> a response can be cached.<\/p>\n<p>Typical directives you\u2019ll use:<\/p>\n<ul>\n<li><strong>max-age=N<\/strong>: how long (in seconds) the browser may reuse the response without re-checking. Example: <code>max-age=31536000<\/code> (1 year) for versioned static assets.<\/li>\n<li><strong>public<\/strong>: response can be cached by <em>any<\/em> cache (browser, CDN, proxy). Use this for assets and anonymous pages.<\/li>\n<li><strong>private<\/strong>: response is specific to a single user (e.g. a profile page); only the user\u2019s browser should cache it.<\/li>\n<li><strong>no-store<\/strong>: do not cache or store this response anywhere (login pages, sensitive dashboards, banking-like data).<\/li>\n<li><strong>no-cache<\/strong>: can be stored, but must be revalidated before reuse. Often combined with ETag\/Last-Modified for dynamic HTML.<\/li>\n<li><strong>must-revalidate<\/strong>: once expired, the cache must contact the origin again before serving the response.<\/li>\n<li><strong>s-maxage=N<\/strong>: like max-age, but only for <strong>shared<\/strong> caches (CDNs, proxies). Browsers ignore it.<\/li>\n<li><strong>immutable<\/strong>: tells modern browsers that this response will never change during its lifetime. Perfect for fingerprinted assets.<\/li>\n<\/ul>\n<p>Example for a hashed JS bundle in a SPA:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">Cache-Control: public, max-age=31536000, immutable\n<\/code><\/pre>\n<p>Example for a dynamic HTML page where you want conditional requests:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">Cache-Control: no-cache, must-revalidate\n<\/code><\/pre>\n<h3><span id=\"ETag_and_conditional_requests\">ETag and conditional requests<\/span><\/h3>\n<p><strong>ETag<\/strong> is a unique identifier for a specific version of a resource. The browser stores it together with the response. On the next request, the browser adds:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">If-None-Match: &quot;the-etag-value&quot;\n<\/code><\/pre>\n<p>If the content hasn\u2019t changed, the server answers:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">HTTP\/1.1 304 Not Modified\n<\/code><\/pre>\n<p>The browser then reuses its cached copy. This is called a <strong>conditional request<\/strong>. Data transfer is minimal, and you avoid re-sending content unnecessarily.<\/p>\n<p>There are two common pitfalls:<\/p>\n<ul>\n<li><strong>Auto-generated ETags with inode info<\/strong> on some web servers leak machine-specific details and can break across clusters. On multi-server setups, configure \u201cweak\u201d or content-hash based ETags instead.<\/li>\n<li><strong>CDNs sometimes ignore or strip ETags<\/strong> if misconfigured. You can rely on <strong>Last-Modified<\/strong> in those cases, or configure the CDN to respect ETags.<\/li>\n<\/ul>\n<h3><span id=\"Last-Modified_and_304_responses\">Last-Modified and 304 responses<\/span><\/h3>\n<p><strong>Last-Modified<\/strong> is simpler: it\u2019s just a timestamp of when the resource last changed:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">Last-Modified: Tue, 10 Dec 2024 13:37:00 GMT\n<\/code><\/pre>\n<p>The browser then sends:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">If-Modified-Since: Tue, 10 Dec 2024 13:37:00 GMT\n<\/code><\/pre>\n<p>If the content hasn\u2019t changed, the server returns <code>304 Not Modified<\/code> just like with ETag.<\/p>\n<p>You can use <strong>either ETag or Last-Modified or both<\/strong>. In many WordPress and Laravel deployments, using Last-Modified based on the file modification time (for static assets) or article updated date (for posts\/pages) is perfectly fine.<\/p>\n<h2><span id=\"Browser_Caching_Strategy_for_WordPress\">Browser Caching Strategy for WordPress<\/span><\/h2>\n<p>WordPress mixes very different content types: static assets from themes\/plugins, dynamic HTML, and potentially WooCommerce cart\/checkout flows. Each needs different caching behavior.<\/p>\n<h3><span id=\"Static_assets_CSS_JS_images_fonts\">Static assets: CSS, JS, images, fonts<\/span><\/h3>\n<p>Goal: <strong>cache aggressively<\/strong>, ideally for months or a year, but only if you pair that with <strong>asset fingerprinting<\/strong> (versioned filenames).<\/p>\n<p>Examples of URLs that should be heavily cached:<\/p>\n<ul>\n<li><code>\/wp-content\/themes\/yourtheme\/style.css?ver=1.2.3<\/code><\/li>\n<li><code>\/wp-content\/plugins\/your-plugin\/assets\/js\/frontend.min.js?ver=2.5.0<\/code><\/li>\n<li><code>\/wp-content\/uploads\/2025\/01\/hero-banner.webp<\/code><\/li>\n<li>Google Fonts or self-hosted fonts under <code>\/wp-content\/uploads\/fonts\/...<\/code><\/li>\n<\/ul>\n<p>On a typical Apache-based <a href=\"https:\/\/www.dchost.com\/wordpress-hosting\">WordPress hosting<\/a>, you can add this to <code>.htaccess<\/code>:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">&lt;IfModule mod_expires.c&gt;\n  ExpiresActive On\n\n  # Images\n  ExpiresByType image\/jpeg &quot;access plus 1 year&quot;\n  ExpiresByType image\/png  &quot;access plus 1 year&quot;\n  ExpiresByType image\/webp &quot;access plus 1 year&quot;\n  ExpiresByType image\/svg+xml &quot;access plus 1 year&quot;\n\n  # CSS, JS\n  ExpiresByType text\/css &quot;access plus 1 year&quot;\n  ExpiresByType application\/javascript &quot;access plus 1 year&quot;\n\n  # Fonts\n  ExpiresByType font\/woff2 &quot;access plus 1 year&quot;\n&lt;\/IfModule&gt;\n\n&lt;IfModule mod_headers.c&gt;\n  &lt;FilesMatch &quot;.(css|js|png|jpe?g|gif|webp|svg|woff2?)$&quot;&gt;\n    Header set Cache-Control &quot;public, max-age=31536000, immutable&quot;\n  &lt;\/FilesMatch&gt;\n&lt;\/IfModule&gt;\n<\/code><\/pre>\n<p>On Nginx (for example on a dchost.com VPS or dedicated server), a similar rule:<\/p>\n<pre class=\"language-nginx line-numbers\"><code class=\"language-nginx\">location ~* .(?:css|js|jpe?g|png|gif|webp|svg|woff2?)$ {\n    expires 1y;\n    add_header Cache-Control &quot;public, max-age=31536000, immutable&quot;;\n}\n<\/code><\/pre>\n<p>This assumes that when you deploy a new version, WordPress (or your build pipeline) changes the file names or query string versions. If you\u2019re still manually editing <code>style.css<\/code> without versioning, consider moving to a proper asset pipeline or at least incrementing <code>?ver=<\/code> on each release.<\/p>\n<h3><span id=\"Dynamic_HTML_posts_pages_and_WooCommerce\">Dynamic HTML: posts, pages and WooCommerce<\/span><\/h3>\n<p>For the main HTML of WordPress pages you have three layers to think about:<\/p>\n<ol>\n<li><strong>Full-page caching<\/strong> (e.g. Nginx FastCGI cache, LiteSpeed Cache, or a WordPress cache plugin).<\/li>\n<li><strong>HTTP caching headers<\/strong> (Cache-Control, ETag\/Last-Modified).<\/li>\n<li><strong>CDN HTML caching rules<\/strong> (if you cache HTML at the edge).<\/li>\n<\/ol>\n<p>If you are not caching HTML at the browser\/CDN level (only at server level), you can keep headers conservative:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">Cache-Control: no-cache, must-revalidate\n<\/code><\/pre>\n<p>and still gain benefits from conditional requests via Last-Modified or ETag. But if you want browsers and CDNs to cache HTML for anonymous visitors, you must be more careful:<\/p>\n<ul>\n<li><strong>Do cache<\/strong> category pages, blog posts, landing pages for logged-out users.<\/li>\n<li><strong>Do not cache<\/strong> <code>\/wp-admin\/<\/code>, <code>\/wp-login.php<\/code>, WooCommerce cart\/checkout, or pages showing personalized content.<\/li>\n<\/ul>\n<p>For WooCommerce in particular, we recommend pairing this article with our CDN-focused guide <a href=\"https:\/\/www.dchost.com\/blog\/en\/cdn-onbellekleme-cache-control-ve-edge-kurallari-wordpress-ve-woocommercede-tam-isabet-ayarlar\/\">The CDN Caching Playbook for WordPress and WooCommerce<\/a>, where we go deep into HTML caching vs checkout safety.<\/p>\n<h3><span id=\"Example_safe_HTML_caching_for_anonymous_visitors\">Example: safe HTML caching for anonymous visitors<\/span><\/h3>\n<p>A pragmatic WordPress pattern:<\/p>\n<ul>\n<li>Use a page cache (server or plugin) for anonymous users.<\/li>\n<li>Serve cached HTML with:<\/li>\n<\/ul>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">Cache-Control: public, max-age=300, s-maxage=600\n<\/code><\/pre>\n<p>This tells:<\/p>\n<ul>\n<li><strong>Browsers<\/strong>: you may reuse this HTML for 5 minutes.<\/li>\n<li><strong>CDNs<\/strong> (shared caches): you may reuse it for 10 minutes.<\/li>\n<\/ul>\n<p>At the same time, instruct your CDN (Cloudflare, etc.) to <strong>bypass cache<\/strong> for:<\/p>\n<ul>\n<li><code>\/wp-admin\/*<\/code><\/li>\n<li><code>\/wp-login.php<\/code><\/li>\n<li><code>\/cart\/*<\/code>, <code>\/checkout\/*<\/code>, <code>\/my-account\/*<\/code> and any URLs that set cart or session cookies.<\/li>\n<\/ul>\n<p>For more detailed WooCommerce-safe rules, see <a href=\"https:\/\/www.dchost.com\/blog\/en\/wordpress-icin-cdn-onbellek-kurallari-nasil-kurulur-woocommercede-html-cache-bypass-ve-edge-ayarlariyla-uctan-uca-hiz\/\">our guide to CDN caching rules for WordPress and WooCommerce HTML<\/a>.<\/p>\n<h2><span id=\"Browser_Caching_Strategy_for_Laravel\">Browser Caching Strategy for Laravel<\/span><\/h2>\n<p>Laravel gives you more explicit control over routes and headers, but the caching principles are the same. Think in terms of <strong>three categories<\/strong>:<\/p>\n<ul>\n<li><strong>Static assets<\/strong> built by your frontend pipeline (Mix, Vite, Webpack).<\/li>\n<li><strong>Blade-rendered HTML pages<\/strong> (classic MVC).<\/li>\n<li><strong>JSON APIs<\/strong> (for SPAs or mobile apps).<\/li>\n<\/ul>\n<h3><span id=\"Static_assets_let_ViteMix_and_Nginx_do_the_heavy_lifting\">Static assets: let Vite\/Mix and Nginx do the heavy lifting<\/span><\/h3>\n<p>Modern Laravel setups with Vite or Mix already produce fingerprinted filenames like <code>app.87a3f1ab.js<\/code>. That\u2019s perfect for aggressive caching. Configure your web server (or CDN) similarly to the WordPress examples:<\/p>\n<pre class=\"language-nginx line-numbers\"><code class=\"language-nginx\">location \/build\/ {\n    expires 1y;\n    add_header Cache-Control &quot;public, max-age=31536000, immutable&quot;;\n}\n<\/code><\/pre>\n<p>Because filenames change on every build, you can safely cache them for a year. When you deploy a new release, the old file is no longer referenced by HTML, so it doesn\u2019t matter that some visitors still have it cached.<\/p>\n<h3><span id=\"Blade_HTML_conservative_but_smart\">Blade HTML: conservative, but smart<\/span><\/h3>\n<p>For pages like dashboards, profile pages, admin panels, or any content that might be user-specific, prefer:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">Cache-Control: private, no-cache, must-revalidate\n<\/code><\/pre>\n<p>You can set this via middleware, for example:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">public function handle($request, Closure $next)\n{\n    $response = $next($request);\n\n    return $response-&gt;header('Cache-Control', 'private, no-cache, must-revalidate');\n}\n<\/code><\/pre>\n<p>For fully public content like a marketing site built on Blade, you can cache similar to WordPress anonymous HTML: set <code>public, max-age=300, s-maxage=600<\/code> and let your CDN handle edge caching of those pages.<\/p>\n<h3><span id=\"Laravel_JSON_APIs_when_to_cache_and_when_not_to\">Laravel JSON APIs: when to cache and when not to<\/span><\/h3>\n<p>APIs fall into three common buckets:<\/p>\n<ul>\n<li><strong>Read-only, rarely changing<\/strong> (e.g. countries list, static configuration, pricing table).<\/li>\n<li><strong>Read-mostly but updated regularly<\/strong> (e.g. product catalog, blog listing).<\/li>\n<li><strong>User-specific or highly dynamic<\/strong> (e.g. <code>\/me<\/code>, notifications, cart APIs).<\/li>\n<\/ul>\n<p>Example headers:<\/p>\n<ul>\n<li>Static config: <code>Cache-Control: public, max-age=86400, immutable<\/code><\/li>\n<li>Catalog: <code>Cache-Control: public, max-age=300, s-maxage=600<\/code><\/li>\n<li>User-specific: <code>Cache-Control: private, no-store<\/code> or <code>private, no-cache<\/code><\/li>\n<\/ul>\n<p>Remember: if your SPA or frontend relies heavily on AJAX\/Fetch, these HTTP headers still apply to those JSON responses, both in the browser and at the CDN edge.<\/p>\n<h2><span id=\"Browser_Caching_Strategy_for_SPA_Frontends_React_Vue_Angular\">Browser Caching Strategy for SPA Frontends (React, Vue, Angular)<\/span><\/h2>\n<p>SPAs behave differently from classic multi-page apps: one <code>index.html<\/code> shell loads JS bundles that then take over routing on the client side. The caching strategy that works well here is:<\/p>\n<ul>\n<li><strong>Short-lived cache for <code>index.html<\/code><\/strong> (the shell).<\/li>\n<li><strong>Long-lived, immutable cache for hashed assets<\/strong> (JS, CSS, fonts, images).<\/li>\n<\/ul>\n<h3><span id=\"indexhtml_treat_it_like_dynamic_HTML\">index.html: treat it like dynamic HTML<\/span><\/h3>\n<p>Your <code>index.html<\/code> file contains references to the current JS\/CSS asset versions and is the entry point to your app. You want users to pick up new releases reasonably fast, so cache it only for a few minutes:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">Cache-Control: public, max-age=300, must-revalidate\n<\/code><\/pre>\n<p>Some teams go even more conservative and use <code>no-cache<\/code> (still allowing conditional requests and 304 responses). That\u2019s fine too, especially during rapid development phases.<\/p>\n<h3><span id=\"JSCSS_bundles_and_assets_immutable_versioned_long-lived\">JS\/CSS bundles and assets: immutable, versioned, long-lived<\/span><\/h3>\n<p>Bundles like <code>main.3f2b9a87.js<\/code>, <code>vendors.0f91c2a4.css<\/code> should be served with:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">Cache-Control: public, max-age=31536000, immutable\n<\/code><\/pre>\n<p>And the same for images and fonts under your SPA\u2019s <code>\/assets\/<\/code> or <code>\/static\/<\/code> directory. Your build pipeline (Vite, Webpack, etc.) should already generate hashed filenames; if not, that\u2019s the first upgrade to plan.<\/p>\n<p>For a more advanced discussion of <code>immutable<\/code> and fingerprinting, we go deeper in <a href=\"https:\/\/www.dchost.com\/blog\/en\/nereden-baslamaliyiz-bir-css-dosyasinin-pesinde\/\">our asset fingerprinting and Cache-Control guide<\/a>.<\/p>\n<h3><span id=\"Service_workers_powerful_but_not_a_magic_bullet\">Service workers: powerful, but not a magic bullet<\/span><\/h3>\n<p>Many SPA stacks offer service workers (PWA features). These can implement custom caching strategies, but they also add complexity. A few practical tips:<\/p>\n<ul>\n<li>Start with <strong>good HTTP caching<\/strong>; then add service workers only where they provide clear value (offline mode, advanced prefetching).<\/li>\n<li>Ensure your service worker has a reliable <strong>versioning and update strategy<\/strong> so users don\u2019t get stuck with old bundles.<\/li>\n<li>Be cautious about caching HTML\/API responses inside service workers; you can easily override or conflict with your HTTP\/ETag logic.<\/li>\n<\/ul>\n<h2><span id=\"How_CDN_Cache_Rules_Interact_with_Browser_Cache\">How CDN Cache Rules Interact with Browser Cache<\/span><\/h2>\n<p>Putting your site behind a CDN introduces a second cache layer: the <strong>edge cache<\/strong>. The browser still caches responses, but now it often gets them from a nearby CDN PoP instead of your origin server. To get the best of all worlds, you need to think about three actors:<\/p>\n<ol>\n<li><strong>Origin server<\/strong>: sets Cache-Control, ETag, Last-Modified.<\/li>\n<li><strong>CDN<\/strong>: respects or overrides those headers, decides what to keep at edge.<\/li>\n<li><strong>Browser<\/strong>: caches according to headers received (usually from the CDN).<\/li>\n<\/ol>\n<h3><span id=\"Origin_vs_edge_who_is_in_charge\">Origin vs edge: who is in charge?<\/span><\/h3>\n<p>CDNs typically offer three broad modes:<\/p>\n<ul>\n<li><strong>Respect origin headers<\/strong>: CDN uses whatever Cache-Control\/Expires the origin sends. Easiest, but sometimes too conservative or too aggressive.<\/li>\n<li><strong>Override with CDN rules<\/strong>: CDN ignores or modifies origin headers; you configure TTLs per path, file type, or response code.<\/li>\n<li><strong>Hybrid<\/strong>: use origin headers generally, but apply exceptions (e.g. \u201ccache HTML for 5 minutes even if origin says no-cache\u201d).<\/li>\n<\/ul>\n<p>For many WordPress and Laravel sites, we like a <strong>hybrid approach<\/strong>:<\/p>\n<ul>\n<li>Let origin define caching for <strong>static assets<\/strong> (long-lived, immutable).<\/li>\n<li>Use CDN rules primarily for <strong>HTML and APIs<\/strong> where you want carefully controlled edge TTL and bypass paths.<\/li>\n<\/ul>\n<p>For a detailed, practical look at CDN TTLs, cache keys and bandwidth savings, see <a href=\"https:\/\/www.dchost.com\/blog\/en\/cdn-onbellekleme-cache-control-ve-edge-kurallari-wordpress-ve-woocommercede-tam-isabet-ayarlar\/\">our CDN caching playbook for WordPress and WooCommerce<\/a>.<\/p>\n<h3><span id=\"Essential_CDN_cache_rules_for_WordPress\">Essential CDN cache rules for WordPress<\/span><\/h3>\n<p>For a typical WordPress + WooCommerce site, we often configure CDN rules along these lines:<\/p>\n<ul>\n<li><strong>Cache static assets<\/strong> (<code>.css<\/code>, <code>.js<\/code>, <code>.jpg<\/code>, <code>.webp<\/code>, <code>.svg<\/code>, <code>.woff2<\/code>) with long TTL (e.g. 1 month at edge) and honor origin headers if they are even longer.<\/li>\n<li><strong>Cache HTML for anonymous users<\/strong> for a short time (1\u201310 minutes), with \u201cignore cookies\u201d for known non-critical cookies (e.g. analytics).<\/li>\n<li><strong>Bypass cache completely<\/strong> for:<\/li>\n<\/ul>\n<ul>\n<li><code>\/wp-admin\/*<\/code>, <code>\/wp-login.php<\/code><\/li>\n<li><code>\/cart\/*<\/code>, <code>\/checkout\/*<\/code>, <code>\/my-account\/*<\/code><\/li>\n<li>Any URL with WooCommerce or session cookies present.<\/li>\n<\/ul>\n<p>Combined with well-tuned origin headers, this yields:<\/p>\n<ul>\n<li>Blazing-fast initial page loads from the CDN edge.<\/li>\n<li>Safe handling of carts, logins and account pages.<\/li>\n<li>Lower origin load and better TTFB metrics.<\/li>\n<\/ul>\n<h3><span id=\"CDN_rules_for_Laravel_and_APIs\">CDN rules for Laravel and APIs<\/span><\/h3>\n<p>For Laravel, CDN rules can be more granular because you control routes explicitly:<\/p>\n<ul>\n<li>Cache <code>\/assets\/*<\/code> or <code>\/build\/*<\/code> aggressively; rely on hashed filenames.<\/li>\n<li>Cache marketing routes like <code>\/pricing<\/code>, <code>\/about<\/code>, <code>\/blog<\/code> at edge for a few minutes.<\/li>\n<li>Bypass or keep very low TTLs on <code>\/api\/*<\/code> endpoints that are user-specific or time-sensitive.<\/li>\n<li>For public APIs that can be cached, configure a cache key that <strong>respects query strings<\/strong> but ignores unnecessary cookies.<\/li>\n<\/ul>\n<p>Advanced CDNs allow you to enable <strong>stale-while-revalidate<\/strong> and <strong>stale-if-error<\/strong> at the edge, serving slightly stale content while fetching a fresh copy in the background or while your origin is having issues. If you want to see this pattern in depth on Nginx, Cloudflare and WordPress, we cover it in <a href=\"https:\/\/www.dchost.com\/blog\/en\/kesinti-caninizi-sikmasin-stale-while-revalidate-ve-stale-if-error-nasil-hayat-kurtarir\/\">our guide to stale-while-revalidate and stale-if-error<\/a>.<\/p>\n<h3><span id=\"CDN_rules_for_SPA_frontends\">CDN rules for SPA frontends<\/span><\/h3>\n<p>For SPAs, CDN and browser caching work extremely well together:<\/p>\n<ul>\n<li><strong>index.html:<\/strong> short edge TTL (1\u20135 minutes), respect origin <code>max-age<\/code>.<\/li>\n<li><strong>Hashed bundles &amp; assets:<\/strong> long edge TTL (weeks or months), <code>immutable<\/code> from origin.<\/li>\n<li><strong>APIs under \/api\/:<\/strong> separate cache rules per endpoint; many will be no-cache or very short TTL.<\/li>\n<\/ul>\n<p>Because SPAs offload most navigation to the browser, ensuring that bundles are served from a local cache or a nearby CDN PoP has a huge impact on the perceived \u201csnappiness\u201d of the app.<\/p>\n<h2><span id=\"Practical_Checklist_and_Common_Gotchas\">Practical Checklist and Common Gotchas<\/span><\/h2>\n<p>Before you roll these changes across production, walk through this short checklist. We regularly use something like this when optimizing sites on dchost.com VPS and dedicated servers.<\/p>\n<h3><span id=\"1_Separate_assets_from_HTML_clearly\">1. Separate assets from HTML clearly<\/span><\/h3>\n<ul>\n<li>Ensure your theme \/ build pipeline outputs assets into predictable directories (<code>\/wp-content\/themes\/...\/assets\/<\/code>, <code>\/build\/<\/code>, <code>\/static\/<\/code>).<\/li>\n<li>Apply long-lived, immutable caching only to those asset paths, not to HTML or API routes.<\/li>\n<\/ul>\n<h3><span id=\"2_Implement_asset_fingerprinting\">2. Implement asset fingerprinting<\/span><\/h3>\n<ul>\n<li>For WordPress, many performance plugins or build tools can handle versioned filenames or <code>?ver=<\/code> query parameters.<\/li>\n<li>For Laravel and SPAs, enable hashed filenames in Vite\/Webpack builds.<\/li>\n<li>Once fingerprinting is in place, you can confidently set <code>max-age=31536000, immutable<\/code> on those files.<\/li>\n<\/ul>\n<h3><span id=\"3_Decide_your_HTML_strategy\">3. Decide your HTML strategy<\/span><\/h3>\n<ul>\n<li>Minimalist: server-side full-page cache only, browsers always revalidate HTML (safe, simpler).<\/li>\n<li>Balanced: 1\u201310 minute HTML cache at browser and CDN for anonymous users, with careful bypasses.<\/li>\n<li>Advanced: use stale-while-revalidate, cache tags, smarter invalidation logic.<\/li>\n<\/ul>\n<p>Whichever you choose, document it so your team knows which headers should be present on each type of response.<\/p>\n<h3><span id=\"4_Configure_CDN_cache_keys_and_exclusions\">4. Configure CDN cache keys and exclusions<\/span><\/h3>\n<ul>\n<li>Cache key should include <strong>path + query string<\/strong> for most static assets and HTML pages.<\/li>\n<li>Ignore cookies that do not affect content (analytics, A\/B testing identifiers where safe).<\/li>\n<li>Never cache when authentication\/session cookies are present unless you are absolutely sure the content is identical for all users.<\/li>\n<\/ul>\n<h3><span id=\"5_Test_with_DevTools_and_curl\">5. Test with DevTools and curl<\/span><\/h3>\n<p>Use browser DevTools (Network tab) and <code>curl -I<\/code> to verify behavior:<\/p>\n<ul>\n<li>Check <strong>Cache-Control<\/strong>, <strong>ETag<\/strong> and <strong>Last-Modified<\/strong> on each type of resource.<\/li>\n<li>Reload pages to see whether you get <code>200<\/code> (from network) or <code>(from disk cache)<\/code> \/ <code>(from memory cache)<\/code> in DevTools.<\/li>\n<li>Send <code>If-None-Match<\/code> or <code>If-Modified-Since<\/code> manually to verify 304 behavior.<\/li>\n<\/ul>\n<h3><span id=\"6_Monitor_real-world_impact\">6. Monitor real-world impact<\/span><\/h3>\n<ul>\n<li>Re-run PageSpeed Insights or WebPageTest and compare <strong>TTFB<\/strong>, <strong>largest contentful paint (LCP)<\/strong> and overall transfer size.<\/li>\n<li>Monitor origin CPU, RAM and bandwidth on your hosting panel or with tools like Netdata\/Prometheus to see reduced load.<\/li>\n<li>Pay attention to error logs during deploys to ensure cache invalidation works as expected.<\/li>\n<\/ul>\n<p>If you\u2019d like a broader view of how hosting choices affect Core Web Vitals, see our article <a href=\"https:\/\/www.dchost.com\/blog\/en\/http-2-ve-http-3-destegi-seo-ve-core-web-vitalsi-nasil-etkiler-hosting-secerken-nelere-bakmali\/\">How HTTP\/2 and HTTP\/3 (QUIC) Affect SEO and Core Web Vitals<\/a>.<\/p>\n<h2><span id=\"Bringing_It_All_Together_and_What_to_Do_Next\">Bringing It All Together (and What to Do Next)<\/span><\/h2>\n<p>Getting Cache-Control, ETag and CDN cache rules right is less about memorizing every directive and more about drawing a clear line between <strong>immutable assets<\/strong>, <strong>semi-dynamic HTML\/APIs<\/strong> and <strong>truly dynamic, user-specific data<\/strong>. Once you label each part of your WordPress, Laravel or SPA stack that way, the actual headers almost write themselves.<\/p>\n<p>On the asset side, fingerprinted filenames plus <code>max-age=31536000, immutable<\/code> give you instant wins in repeat visits. On the HTML\/API side, short max-age or no-cache combined with ETag\/Last-Modified and, optionally, CDN features like stale-while-revalidate give you a good balance between freshness and performance. If you pair this with a well-chosen CDN edge strategy and solid hosting resources (whether on shared hosting, VPS, dedicated or colocation with dchost.com), you can serve fast experiences consistently, even under load.<\/p>\n<p>If you want to go deeper, we recommend reading <a href=\"https:\/\/www.dchost.com\/blog\/en\/cdn-nedir-ne-zaman-gerekir-trafik-ve-lokasyona-gore-karar-rehberi\/\">our introduction to when a CDN really makes sense<\/a> and combining it with the caching guides linked above. And if you\u2019re planning a new architecture or migration, our team at dchost.com can help you choose a hosting setup where your caching strategy, PHP stack and CDN all work together instead of fighting each other.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>Page speed problems on WordPress, Laravel or modern SPA frontends are often blamed on PHP, the database or the hosting plan. In reality, a surprising amount of \u201cslowness\u201d comes from missing or incorrect browser caching. Every time a visitor reloads your site, their browser must decide: \u201cCan I reuse what I already have, or do [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":3578,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-3577","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-teknoloji"],"_links":{"self":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/3577","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/comments?post=3577"}],"version-history":[{"count":0,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/3577\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media\/3578"}],"wp:attachment":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media?parent=3577"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/categories?post=3577"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/tags?post=3577"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}