Basj
Basj

Reputation: 46247

How to let the browser or PHP cache a fetch() request?

This is basically the opposite of fetch(), how do you make a non-cached request?.

Let's say we have client-side:

<div onclick="fetch('/data.php').then(r => r.text()).then(s => console.log(s));">Click me</div>

and server-side in PHP:

<?php echo "hello"; /* in reality, this can be much more data 
                       and/or updated later with other content */ ?>

When clicking multiple times on Click me, the request is done multiple times over the network with request response code "200".

Thus, no caching has been done by the browser / by PHP:

enter image description here

How to prevent this data to be transferred multiple times if it is in fact not needed here?

Potential solution: Since /data.php can be updated on the server, the server could serve the data requests with a datetime metadata timestamp, that I could store in a client-side JS variable timestamp. Then before doing the next fetch request for /data.php, I could do a preliminary request

fetch('/get_last_modification_date.php').(r => r.json()).then(data => {
    if (data['timestamp'] != timestamp)
        fetch('/data.php')...       // if and only if new data is here
});

Thus, we only do a fetch('/data.php')... if the timestamp of this data is more recent than the one we already had.

This would work, but it seems we are manually doing something that could be done by the browser+PHP cache mechanism.

How to ask PHP and the browser to do all this caching for us?

Upvotes: 4

Views: 1339

Answers (4)

user5781320
user5781320

Reputation:

use + Date.now() for unique requests; and don't use static text in experiments like this ,use dynamic content in output:

i did my own experiment(in xampp windows) base on your question (in firefox) look in console and then in server's headers section ..

index.php :

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="Generator" content="Custa">
<title></title>
</head>
<body>
<div onclick="fetch('data.php?'+ Date.now()).then(r => r.text()).then(s => console.log(s));">Click me</div>
</body>
</html>

data.php :

<?php

echo microtime(true);

?>

Upvotes: 0

Olivier
Olivier

Reputation: 18220

fetch() behaves the same as a regular HTTP request, so you can just apply the standard HTTP rules regarding caching. For example you can use the If-Modified-Since mechanism, that I will explain.

When a server returns the following headers for a resource:

Cache-Control: no-cache
Last-Modified: <date in RFC2616 format>

then, on subsequent requests, the browser will send an If-Modified-Since header with the Last-Modified date. If the server returns a 304 (Not Modified) status, then the browser will use the cached version of the resource.

Remark: despite its name, the no-cache directive doesn't mean that the resource can't be cached; it just means that the browser must validate it with the server before using it.

To illustrate this, I will assume that you want to send the contents of some data.txt file:

<?php
// Get the modification time of the resource
$dataTime = filemtime('data.txt');

if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']))
{
    $sinceTime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
    if($sinceTime==$dataTime)
    {
        // The browser already has the latest version of the resource
        header('HTTP/1.1 304 Not Modified');
        exit;
    }
}

// Send the resource
header('Cache-Control: no-cache');
header('Last-Modified: '.gmdate('D, d M Y H:i:s T', $dataTime));
header('Content-Type: text/plain; charset=UTF-8');
readfile('data.txt');
?>

Upvotes: 3

DevelJoe
DevelJoe

Reputation: 1332

I see two solutions to this; and I've implemented both already throughout my applications, so they both work.

Solution A : Specify the cache value when using the fetch API, instead of just passing the link from which you want to fetch a given resource / post specific data / whatever. Choose from default, no-store, reload, no-cache, force-cache or only-if-cached according to your needs, as explained in the first link.

Combine this with your mentioned timestamp to accordingly decide if the cache is still fresh/ stale etc. (as explained in the first link), and you're good to go.

Solution B : I've implemented in this in some scenarios where for example multiple kind of GET requests are allowed to the same endpoint, while however some requests (like GET https://example.org/api/v1/users) make subsequent ones, that are further narrowed down (like GET https://example.org/api/v1/users/employees/4), obsolete.

In such a scenario B, I initiate a JS object, like so:

const users = {};

Suppose I then for example fetch the users via GET https://example.org/api/v1/users, and that that requests also retrieves the employees for this API (for whatever reason) under an employees key. I populate users with the retrieved data, hence also with an employees key, holding for example 5 employees.

Then, on subsequent requests, like GET https://example.org/api/v1/users/employees/4; I check for example in this case if the users object already retrieved the employees by doing stuff like:

if (!users.hasOwnProperty('employees')) { // Only fetch your data in this case }

To only make a request to the server if the data is not already present in my local js environment. You then may add a timestamp property to your users object according to your needs, and check in the condition above if the timestamp of subsequent requests is below that timestamp, e.g. the request should be retrieved from the local variable, and not from the server. This kind of mimicks your cache, it's the same principle.

Of course, solution B really depends on how you've setup your server responses etc., so I'd say solution A is the standard approach.

For further infos regarding solution A, you probably also want to have a quick read through when is a HTTP Request's cache still considered fresh?, or rather just checkout the entire HTTP Cache docs quickly.

And you may also want to have a look at this question, as the first answer shows an example of how you could also explicitly set the Cache-Control Header. In this sense; you should consider the value of the max-age key of the Cache-Control header you set as your timestamp. This is how browsers generally handle caching natively.

Upvotes: 2

Dan Mullin
Dan Mullin

Reputation: 4415

A simple way is to wrap the call to fetch

Ideally this would be in a class or closure, but global variables now for brevity’s sake.

var fetched = false;
function clickMe(someURL) {
    if (fetched) { return; }
    fetched = true;
    fetch(someURL).then() // etc.
}

// onclick="clickMe('/data.php')"

Upvotes: 1

Related Questions