Forum Moderators: phranque

Message Too Old, No Replies

Prefetching a file

         

csdude55

12:17 am on Feb 20, 2020 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



In an attempt to make that first page load a smidge faster, I'm including the CSS stylesheet via PHP on the first view, then setting a cookie, then loading the stylesheet at the bottom to cache it. Future page loads will see the cookie and use the traditional <link rel=...>, because the CSS has (or should be) already loaded.

I know, I know... micro optimizing. But I've done so many of these little micro changes that they've added up to be significant :-)

I first set this up using jQuery 1.8 or so, and using $.load('/path/to/css.css') at the bottom worked fine. But now it looks like .load() is deprecated as of around 3.x and it no longer works. So I'm revisiting the concept to see what's better.

I see that I can use prefetch:

<link rel="prefetch" href="/path/to/css.css">

It doesn't work on <=IE10, though:

[caniuse.com...]

And apparently it's not super reliable, even with browsers that do support it... which makes me think it might be removed in the future:

[css-tricks.com...]

But it would be pretty easy to implement in PHP:

if ($_COOKIE['css_exists'])
echo <<<EOF
<link rel="stylesheet" href="/path/to/css.css">

EOF;

else {
echo <<<EOF
<style>

EOF;

include "/path/to/css.css";

echo <<<EOF
</style>

<link rel="prefetch" href="/path/to/css.css">

EOF;

setcookie('css_exists', '1', 365, '/');
}

So that's an option, but maybe not a great one.

Another possible solution, I think that I can use jQuery's $.get('/path/to/css.css') at the bottom of the page to do the same thing?

<script>
$.get('/path/to/css.css');
</script>


If so, am I correct in understanding that I could create a callback function that would run AFTER /path/to/css.css has completely loaded?

[api.jquery.com...]

That wouldn't be bad, I could set a cookie there and then if the cookie doesn't exist I can load it via PHP at the top:


<?php

echo <<<EOF
<head>
...

EOF;

if (!$_COOKIE['css_exists'])
include '/path/to/css.css';

echo <<<EOF
<script>
// common setCookie() function, I think
function setCookie(c_name, value, expiredays) {
if (!expiredays) expiredays = -1;

var exdate = new Date();
exdate.setDate(exdate.getDate() + expiredays);

document.cookie = c_name + '=' + escape(value) + '; path=/' + ((expiredays == null) ? '' : '; expires=' + exdate.toGMTString());
}
</script>
</head>

<body
...

<script>
$(function() {
$.get('/path/to/css.css', function() {
setCookie('css_exists', '1', 365);
});
});
</script>

</body>
</html>
EOF;
?>


So what do you guys and gals think, would you use prefetch, $.get(), or some other solution that I haven't considered?

csdude55

6:12 am on Feb 20, 2020 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



Well, after several tests using Chrome DevTools, I don't think that my browser is caching my CSS at all! Every test I ran shows that it's downloading the CSS file every time, and I can't see why.

Even when I removed everything and just used a simple <link rel="stylesheet" href="...">, it loaded from the server each refresh. And if I did a prefetch and then added the stylesheet right after (or vice versa), I had 2 separate connections.

My only 1am-induced guess was something in the .htaccess, but I haven't touched that in YEARS! In my root directory I have:

# plugged in by cPanel while gzipping
<IfModule mod_deflate.c>
AddOutPutFilterByType DEFLATE text/html text/plain text/css text/javascript application/javascript application/x-javascript text/xml application/xml application/xml+rss application/vnd.ms-fontobject application/x-font-ttf
</IfModule>


And in the /public_html/ directory:

<FilesMatch "\.(ico|css|js|jpg|jpeg|png|gif)$">
Header set Cache-Control "max-age=31536000, public"
</FilesMatch>

That looks to me like everything should make the CSS file cache better, not worse! So I'm kind of at a loss.

phranque

6:38 am on Feb 20, 2020 (gmt 0)

WebmasterWorld Administrator 10+ Year Member Top Contributors Of The Month



what are the actual relevant headers sent with the css file request/response?

csdude55

2:11 pm on Feb 20, 2020 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



I'm not sure, where would I find that? Under DevTools > Network, my test page shows:

[mysite.com...]
Name: test.php
Status: 200
Type: document
Initiator: Other
Size: 446b
Time: 91ms

[mysite.com...]
Name: css.css
Status: 200
Initiator: test.php
Size: 9.4kb
98ms

I changed "$_COOKIE['css_exists']" to "$_GET['css_exists']", so the parameter in the query string is changing it from "include" to "<link rel=...>".

I tried inserting an image, too, and it is loaded on every page load, too. I get virtually the same results on every page, so it's definitely not caching.

NickMNS

4:13 pm on Feb 20, 2020 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



In the top menu of the network tab to the left of the red "record" icon there are various check boxes, there is one for "disable cache" is it checked?

To see the header info, click on the "name" eg: test.php and the view will change, you should see to the right "Headers", "Preview" "Response" etc... The header info will be in the "Headers" tab.

csdude55

7:30 pm on Feb 20, 2020 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



In the top menu of the network tab to the left of the red "record" icon there are various check boxes, there is one for "disable cache" is it checked?

Nope :-( Good thought, though. I saw that last night and DID check it at one point, but unchecked it about an hour before posting.

To see the header info, click on the "name" eg: test.php and the view will change, you should see to the right "Headers", "Preview" "Response" etc... The header info will be in the "Headers" tab.

Ahh, I gotcha. Here's what I get for the CSS file, with server info and my personal data removed:

Request Method: GET
Status Code: 200 OK
Referrer Policy: no-referrer-when-downgrade
Accept-Ranges: bytes
Cache-Control: max-age=31536000, public
Connection: Keep-Alive
Content-Encoding: gzip
Content-Length: 9267
Content-Type: text/css
Date: Thu, 20 Feb 2020 18:20:32 GMT
Keep-Alive: timeout=5, max=20
Last-Modified: Wed, 19 Feb 2020 19:10:08 GMT
Server: Apache
Vary: Accept-Encoding,User-Agent
Accept: text/css,*/*;q=0.1
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cache-Control: no-cache
Connection: keep-alive
Pragma: no-cache
Sec-Fetch-Dest: style
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: same-origin


For future readers, the "Last-Modified" line is the time of the last upload of the stylesheet, not the time that it was downloaded by the browser. I was excited for a second until I realized that...

But these are stand-outs as likely problems:

Cache-Control: no-cache
Pragma: no-cache

Where would that be coming from? My test page doesn't have any assigned headers, and I looked through the .htaccess files through each parent directory and the only reference to "cache" is what I posted before. I did a scan of my entire /www/ directory and didn't find any reference to "cache", either.

The only reference to "cache" I could find in the PHP configuration was:

; Set to {nocache,private,public,} to determine HTTP caching aspects
; or leave this empty to avoid sending anti-caching headers.
session.cache_limiter = nocache

; Document expires after n minutes.
session.cache_expire = 180

So I'm still at a loss on where this is originating.

csdude55

10:42 pm on Feb 20, 2020 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



Wait, no, forget all of that... I cleared cache and restarted my computer, and now it's caching properly. So that was just a ID-ten-T error on my end.

So back to the question of prefetching...

I've tried every variation I can think of now, and it looks like this is the simplest:

echo <<<EOF
<link rel="prefetch" href="/path/to/css.css">

EOF;

if (!$_COOKIE['css_exists']) {
$style = file_get_contents('/path/to/css.css');

if ($style) {

// homemade minimizer, removed comments and unnecessary spaces
$pattern = '#/\*[^*]*\*+([^/][^*]*\*+)* /|\r\n|\r|\n|\t#';
$style = preg_replace($pattern, ' ', $style);
$style = preg_replace('#\s{2,}#', ' ', $style);
$style = preg_replace('#^\s|\s$|\s?([=),;+])\s?#', '$1', $style);

echo <<<EOF
<style>
$style
</style>

EOF;

setcookie('css_exists', '1', 365, '/');
}
}

if ($_COOKIE['css_exists'] || !$style)
echo <<<EOF
<link rel="stylesheet" href="/path/to/css.css">

...
EOF;

If css.css is already cached then prefetch shows disk cache and takes 1ms to load, so it's not a big deal to load it at the top of the page regardless.

Using jQuery was identical in speed, though:


if (!$_COOKIE['css_exists']) {
$style = file_get_contents('/path/to/css.css');

if ($style) {

// homemade minimizer, removed comments and unnecessary spaces
$pattern = '#/\*[^*]*\*+([^/][^*]*\*+)* /|\r\n|\r|\n|\t#';
$style = preg_replace($pattern, ' ', $style);
$style = preg_replace('#\s{2,}#', ' ', $style);
$style = preg_replace('#^\s|\s$|\s?([=),;+])\s?#', '$1', $style);

echo <<<EOF
<style>
$style
</style>

EOF;
}
}

if ($_COOKIE['css_exists'] || !$style)
echo <<<EOF
<link rel="stylesheet" href="/path/to/css.css">

...

<script>
$(function() {
$.get('/path/to/css.css', function() {
setCookie('css_exists', '1', 365);
});
});
</script>

</body>
</html>
EOF;

So if anyone can confirm whether the callback function loads after css.css is fully loaded, then that would be the better way to go. If it runs as soon as the $.get begins, though, then I don't know that there would be any advantage to using it over the first one. Unless maybe setting a cookie in Javascript would be faster than setcookie() in PHP?

csdude55

12:00 am on Feb 21, 2020 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



So if anyone can confirm whether the callback function loads after css.css is fully loaded, then that would be the better way to go.

Sorry to keep replying to myself, but I THINK that I've been able to confirm that the callback DOES run after the $.get() file has loaded.

This was my test:

// at the bottom of the main page
<script>
$.get('https://www.mysite.com/test.js', function() {
console.log('made it to B');
});
</script>

// test.js
// I chose a high number to make sure it took some time to load, but it turns out that I could have
// just used 1000 and been sure
for (var i = 0; i < 100000; i++)
console.log(i);

The results printed numbers 0-99999, which took about 30 seconds, and THEN it printed "made it to B". So the for() loop ran in its entirety before going to the callback.

Based on this, using the $.get() option would be the better choice, because I can be relatively confident that the sheet has successfully cached before setting the cookie.

tangor

2:11 am on Feb 21, 2020 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



I admire those those do the "nocks and crannies" kind of coding. Keep after it csdude55!

And keep us posted!

csdude55

3:43 am on Feb 21, 2020 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



I enjoy it, too :-) Coding has gotten kind of boring, it's been same-ol-same-ol for 6 or 7 years now, so these types of projects have been challenging and fun again.

After this update, I have my homepage first-view load time down to 1.8s usable, 2.042s complete! Second view, it's complete in 1.822s. Not counting Google ads, of course; I can't control them so I take them off for speed testing. I don't know that I can tweak it much more than that! LOL

tangor

9:14 am on Feb 21, 2020 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



Knowing what you have that's very good times. Mine load faster, then again they are extremely lean in code (no js of any kind, limited perl and I mean limited!) and code+css generally weighs in at 50k or less, might have a 40k image added. the header footer logo all load once and then are in browser cache after ...

Different stroke for different folks AND needs.

Anything that can shave time when utilizing third party in addition is cream on top... and more important if your users are mobile instead of desktop/tablet.

Your little adventures into Time are always fascinating ... and thought provoking as well. Appreciate you sharing these adventures with us!

robzilla

3:16 pm on Feb 23, 2020 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



I THINK that I've been able to confirm that the callback DOES run after the $.get() file has loaded.

As noted in the jQuery docs [api.jquery.com], it is a "callback function that is executed if the request succeeds".

// homemade minimizer, removed comments and unnecessary spaces

I would suggest pre-optimizing the CSS file, since those preg_replace() calls are expensive. You could cache the minimized version on the first request, or (automatically) generate a minified version, e.g. css.min.css, alongside css.css after you're done editing it. I often use YUI Compressor (a single .jar file) for that. You might even pre-compress the files with gzip and/or brotli while you're at it.

csdude55

8:16 pm on Feb 23, 2020 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



As noted in the jQuery docs [api.jquery.com], it is a "callback function that is executed if the request succeeds".

I read that, too, but I find that to be kinda vague. How do they define "request succeeds"? The implication is that it reads the header, and once it gets a 200 then the function continues. But when does that header get returned? In my test the documents fetched ran in its entirety before the function was executed.

I would suggest pre-optimizing the CSS file, since those preg_replace() calls are expensive.

Excellent point! I'm still in production so I can't really minimize the CSS file yet (or the Javascript file that I'm doing the same way), but when I go live I'll just create a minified copy and be done with it.

csdude55

6:20 pm on Feb 24, 2020 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



For future readers, I also came across this PHP function that might be better for removing comments and minimizing on the fly:

function no_comments($tokens) {
$remove = [];
$suspects = ['T_COMMENT', 'T_DOC_COMMENT'];
$iterate = token_get_all('<?php ' . PHP_EOL . $tokens);

foreach ($iterate as $token) {
if (is_array($token)) {
$name = token_name($token[0]);
$chr = substr($token[1], 0, 1);

if (in_array ($name, $suspects) && $chr !== '#')
$remove[] = $token[1];
}
}

return str_replace($remove, null, $tokens);
}

no_comments($style);


I'm not sure that token_get_all(), is_array(), token_name(), substr(), in_array(), and str_replace() combined would be faster than 3 preg_replace() functions, but maybe. It's worth testing for your application, anyway.

Of course, once everything is finished then it's much wiser to permanently minimize the code. But I'll end up beta testing everything for a month with my sites users and expect to constantly have to tweak the CSS and JavaScript, so for me this will just be a temporary solution during that phase.

robzilla

9:11 pm on Feb 24, 2020 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



You could minify on-the-fly once, on the first hit, then store the results in a separate file and use that for subsequent hits until the source file changes (e.g. by comparing the filemtime() of both). Similarly, you could have a deamon watch the file system for changes to the source file and automatically minify and compress it after any edits. I'd certainly prefer using a specialized tool like YUI Compressor (Closure Compiler for js) over a PHP solution.