Webpage performance notes

From Helpful

This article/section is a stub — probably a pile of half-sorted notes and assertions some of which may well be wrong, and not verified as a whole. Feel free to add or refine.

Contents

Page load

Page load time depends on various things, though these days many of them are something to do with scripting. See also Javascript/DOM_notes#Script_behaviour_.28particularly_external_scripts.29.


Persistent connections and pipelining

This article/section is a stub — probably a pile of half-sorted notes and assertions some of which may well be wrong, and not verified as a whole. Feel free to add or refine.

Avoids unnecessary connections and latencies. See HTTP notes for details.


The most important thing to know about this is probably that you should not forget to add Content-Length in your dynamically generated responses, whenever you can.

Saving Bandwidth by Hosting Elsewhere

Particularly static content such as images, can be hosted elsewhere to offload your webserver that's already busy enough with dynamic pages - and with usually at most two or four connections per host. If you serve static content (e.g. many style images) from multiple hosts, this allows somewhat more parallel delivery.

It may also help avoid some unnecessary transfer - for example, if you use cookies and many requests (e.g. many images), they may combine to mean a bunch of KB of avoidable transfer.

It may also make your management of Vary and other cache directives simpler (since static content is non-Vary, Cache-Control public).


You can of course set up your own static media server. (Note that there are various tricks you can use to lessen disk IO in such a server)

Saving Bandwidth by Caching

Allowing browsers cache static data reduces unnecessary repeated transfer of data. Allowing proxies to do so lowers latency whenever they apply.


The most useful way is to use a mechanism that lets browsers ask "Okay, this is what I saw last, has it changed?" (by date or by content identifier) and have the server you a tiny packet just telling you that no, it hasn't changed, in which case the browser can use the cached version, or if the data has changed, send the new data. This allows clear semantics and allows handling of these details in a single request.

You can also tell the browser that the browser can assume that the content will simply NOT change until some particular future date, that it needn't bother to check in until then. note this is overzealous in some situations, and overrules any date/content-based check.


There is usually a mechanism already in place for served files. You can also implement it for dynamic data, but that needs work and thought on the coder's side.


Note that there are two types of cache: The browser's cache, and transparent caches (proxies) all over the internet. The effect is the same, and the largest difference is that content marked private will not be cached by the transparent ones, but the browser will. (Note this can be more finely controlled by the server via the Vary header)


For example, for static content, you can easily leverage proxies with some specific headers. For example, if you have a static /img directory, it probably makes sense to (assuming you have mod_headers) tell apache something like:

Header set Cache-Control "public"



HTTP 1.0 and HTTP 1.1

Getting cache headers right is a minor art, since different headers apply to different versions of HTTP and therefore different browsers. The HTTP 1.0 specs had a few cases with no behaviour defined, which has led to some creative behaviour in clients and proxies. In part, HTTP1.1 is simply better defined (e.g. 13.2, Expiration Mechanisms), and it also has some more powerful features. It seems, however, that there is still considerable confusion about the differences and what mechanisms should be used.


To summarize the mechanisms, HTTP 1.0 has:

  • Last-Modified, and If-Modified-Since:
  • Expires:
  • Pragma: no-cache, forcing the request to go to the origin server, and not from not from a cache (including things like Squid)


HTTP 1.1 has:

  • Last-Modified: and If-Modified-Since:, (like HTTP 1.0)
  • Expires: (like 1.0)
  • Cache-Control:, which allows both (origin) server and agent to specify things that apply to chaces external to them (sometimes separately between agent cache and proxy cache), including:
    • max-age, which can be used as a relative time measure instead of the absolute-date Expires - but unlike Expires, is considered more of a hint(verify)
    • expirations changes
    • how important stale-based re-checks are to serving cached content
    • have a public/private(/no-cache) distinction. Public means it may be cached on transparent caches, private means only the user's browser may cache it, and no-cache (as before) that it must not be cached at all.
  • an entity tag (ETag) system, which is a content identifier thing

Cache awareness details

When writing code to be aware of caches, you should consider things like:

  • Proxy caches are fairly unlikely to cache reponses for POST requests. You may want to consider this in your site design.
  • Don't count on all requests from a logical user coming from the same host


Disabling cache

You will want to do this when you want to make sure dynamically generated content always comes from the server. (If you can identify non-chances, look at a mechanism to use that fact, like modification date or etag)


To make sure you are disabling the case in the face of HTTP1.1, HTTP1.0 and older HTTP1.0-only proxies, you'll probably want most of:

  • Pragma: no-cache for HTTP 1.0 browser and proxies - but Pragma is often not honoured, so so you usually want:
  • Expires: (date in the past) Giving an Expires value of 0 or an invalid date should be interpreted as immediate expiration. It's slightly safer to just pick a valid date squarely in the past, though.
  • Cache-control: no-cache (for HTTP 1.1: current browsers and decent proxies)


Modification checks

The currently usual way of doing caching is always doing checks.

Browsers will send the command "Send me content only if it has changed since (specific date), otherwise give me a body-less 304 Not Modified response)"

Specifically, content served with a Last-Modified header will have that header recorded in the cache, so it can ask the server If-Modified-Since with that recorded date.


This generally a good solution, although if you have a lot of separate small files to check for a page, it may not matter so much - they lead to a series of If-Modified-Since requests and 304 responses. You don't lose time in transfer of the content, but you still lose time in that serial back-and-forth. This is one reason you should always minimize the amount of external resources.

It works best on file serving or other things that can be easily judged to not have changed since some date. For non-dynamic content, the file's last change date works perfectly well, but in modern dynamic content sites, this is far less obvious and you'll probably want to use the content-based Etag system instead; see the section on it.


Expires

Expires means the server tells the browser that "you should not check with me at all until [date]," which has both upsides and downsides.

On files that will never change, the "use the cache if you come back within x time" is nice.

Note that it does not necessarily apply well to content that is expected to change, because of the granularity. If you sent "won't change for a month" {{{1}}} two minutes ago and you update the file now, the browser may show old content for almost a month. Or not, when it's pushed out of its cache. In short, you can only guess when something will update.

In some cases this can cause other problems. For example, a web page style often consists of css, javascript, images. If you have plus-1-week Expires on each and change the style significantly, (different) clients may use (different) mixes of old and new versions. (also depending on proxy interaction). In the worst case (when there are significant dependencies between such items), the site may appear randomly broken for a week. Note that you can avoid this specific problem by using different URLs for the new style files.


You can certainly usefully use Expires to lighten the load slightly on other mechanisms. For example, for people that are actively clicking around on your website, even an 'access time plus 3 minutes' Expires will make repeat loads of some resources low-latency (lower than a lot of If-Modified-Since/304, since that's a network interaction for each item).
However, both sides' computer clocks must be set accurately for this to work on a few-minute scale (and not be timezone-ignorant), so in reality, you'll want to look at Cache-Control: max-age instead.(verify)

Vary

Vary tells proxy caches what part of a request it should consider when determining whether two requests are identical, to decide whether the request should be served by the origin server or can be served by the cache.

Examples:

  • Pages that differ depending on users that are logged in should Vary on cookies as different cookies imply different pages and no one should ever get another user's page.
  • If you serve different languages (using Accept-Languages), you want to vary on those, or caches might give everyone whatever language is currently in the cache (that came to be there via the preferences of the user that visited the page when it wasn't yet in the cache).

Caches may choose to cache the various versions, or not cache them at all, so to get the most out of caching (...proxies), you generally want to vary on as little as possible so that the cache applies exactly as often as it usefully can.

Cache-Control

This article/section is a stub — probably a pile of half-sorted notes and assertions some of which may well be wrong, and not verified as a whole. Feel free to add or refine.

A HTTP1.1 header. Requests and responses can use this, and different parts apply.


(Note: for 'shared cache' you can usually read 'transparent proxy cache', for 'private cache' you can read 'browser cache')


In server responses:

  • public: may be cached, in a shared as well as private cache
  • private: May be stored in a browser cache but not in a shared proxy cache
  • no-store: may not be stored in even the browser's cache
  • no-cache: may be stored, but must be checked for freshness every request (that is, caches must never return stale cached requests even if they are configured to do so). Useful for public content that requires authentication.
  • must-revalidate: Force strict obeying of your values (*)
  • proxy-revalidate: The same, by applying only to proxies
  • no-transform: tell proxies not to change the data (e.g. recompressing images for space)
  • "max-age": a hint value
  • extensions: all other values are ignored if not understood - allowing values belonging to specific extensions to be ignorable

(*) HTTP allows agents to take liberties with most values like max-age and Expires when evaluating freshness. To force them to obey to your values:


In client/proxy requests:

  • max-age: "I'm willing to take a version that's up to n seconds old" (note: not 'past expiration', that's:)
  • max-stale: "I'm willing to take a version that's up to n seconds past expiration"
  • min-fresh: (verify)
  • only-if-cached: Apparently used among sibling proxies, to synchronize content without causing origin requests(verify)
  • no-store and no-cache: same idea as the server-sent versions, but now requested by the client. (This may not necessarily be (properly) implemented by proxy caches(verify))
  • extensions

ETag

ETag ('entity tag') is a system centered around a simple identifier, which you can use to tack on particular versions. The idea is that instead of identifying changes based on last modification date, you base it on the content itself.

You can use this for if-changed style caching (even dynamically keep several versions, if you want to for some reason).

The server sends Etag: something, the client remembers this on cached items and queries If-None-Match: something or If-Match: something.


Notes:

  • This can be combined with byte-range operations. That is, instead of Range, you can If-Range, allowing you to ask "send me parts that I am missing, or if things have changed, send me the whole new version" in a single request.
  • You can also use this for conditional execution, particularly to conditionally avoid page-serving that has side effects (PUT, GET with database access, etc.)
  • If you generate dynamic content predictably, it is usually fairly simple to write your own ETag system (perhaps using a hash of the content you serve), to avoid recalculating the content unnecessarily.
  • Web servers automatically creates ETags when they server static files
    • Apache 2 uses a combination of a file's inode, modification time, and size. (see also FileETag)
    • IIS bases it on modification time and, er, something else.
    • (When distributing content, you may want to use your own logic so that the same file on different systems is also considered the same)


browser bugs

Note there are also a few client bugs that lead to annoying extra cases, most of which are present in IE.

  • this IE bug makes <meta http-equiv> unreliable -- and numerous proxies don't parse HTML so won't see meta tags anyway. This means that if you want things to work and stay simple, you should use only real HTTP headers.

IE also has a history of not adhering to cache headers, using cached versions when it shouldn't. Particularly in dynamic sites this means workarounds are somewhat common, such as adding a random number to the request to pretend it is another page.

IE also soemtimes has a problem handing files to external apps, deleting it (because it is a non-cacheable item) before it can be read by that application.

mod_expires

In apache, you can use mod_expires, which allows you to set a minimum time in cache (or some time since last file change).

You can have settings at server (not advised!), vhost (if you know what you're doing), and directory/htaccess level, and can set it per MIME type - and practically also per extension.


Besides the inherent overkill behaviour of the Expires header, there seem to be a few gotchas:

  • It seems to apply to all content regardless of whether the source was dynamic or not, which is bad on dynamic sites.
  • It does not interact with other cache headers, which is regularly also not what you want.
  • Server-level ExpiresByType overrides more specif (e.g. directory-level, FilesMatch) ExpiresDefault. This is one reason you shouldn't setting things at server level even when you're not using vhosts.


Example:

ExpiresActive On
 
#That's the shorthand form for 'access plus 0 seconds' 
ExpiresDefault A0
#I prefer the longer form used below, as it is more readable. 
 
<Directory /var/www/foo/>
  ExpiresByType text/css     "modification plus 5 minutes" 
  ExpiresByType image/png    "access plus 1 day"
  ExpiresByType image/jpeg   "access plus 1 day"
  ExpiresByType image/gif    "access plus 1 day"
  ExpiresByType image/x-icon "access plus 1 month"
</Directory>
 
<Directory /var/www/foo/static>
  ExpiresByType image/png    "access plus 1 day"
  ExpiresByType image/jpeg   "access plus 1 day"
  ExpiresByType image/gif    "access plus 1 day"
  <FilesMatch "\.(xm|jp2|mp3)$">
    ExpiresDefault "access plus 3 months"   
  </FilesMatch>
</Directory> 
 
<Directory /var/www/foo/weeklycolumn>
  ExpiresDefault "modification plus 6 days"
  # This is a *file* based timeout, independent of when it was accessed. 
  # Beyond that time the agent will *always* check, so this is most useful 
  # for data that actually changes regularly
  # If this were 'access', clients might not check until, in the worst case,
  # six days after you actually updated the page!
</Directory>

Notes:

  • To be compatible with servers that don't have the module, always wrap in a module test,
    <IfModule mod_expires.c>
    and
    </IfModule>
    .


  • know the difference between 'access' and 'modification' - it's not a subtle one.
  • Be conservative and don't use Expires as your only caching mechanism. Clients will fall back to If-Modified-Since anyway (and if they don't, that is the mechanism you should be focusing on) so you're basically setting the interval of real checks.
  • Things like styles and scripts should not have long expire times - old styles will apply to previous visitors for a while after you may have changed them completely. . (unelss of course you use new filenames for each))

Manual apache statements

mod_expires is so basic that it can only set Expires, no other cache control headers.

In some cases, you may want to abuse mod_headers, for example:

<FilesMatch "\.(html|htm|php)$">
  Header set Cache-Control "max-age=60, private, proxy-revalidate"
</FilesMatch>
<FilesMatch "\.(jpg|jpeg|png|gif|swf)$">
  Header set Cache-Control "max-age=604800, public"
</FilesMatch>

Note that Cache-Control is a HTTP1.1 header

Saving Bandwidth with Compression

HTTP compression will often easily reduce HTML, CSS, and javascript to 20-40% of its original size, depending on the method of compression (gzip and deflate/zlib) and the content.


Browser rendering speed gains are negligable unless the data is relatively large or the client is on a low-bandwidth connection, but the reduced bandwidth use is useful, even when only in terms of server bandwidth bills.


Gotchas:

  • IE6 and previous never cache compressed pages (yes, this is a stupid bug). Whenever there is repeat downloading of fairly small files, caching is more important than compressing (to both sides). This basically means that you never want to send compressed content to IE, so you'll want some browser-specific filtering.
  • IE may decide that compressed error pages are too small to be real(verify), and decide to show its own. You may want to avoid compressing these.


Notes:

  • In some implementations gzipping implies that the document can only be delivered as a whole (and not shown incrementally in the browser as it is downloaded). In other implementations, gzipped delivery can happen in chunks.
  • If you code compression yourself, you should check the Accept-Encoding: header for which compression format, if any, the browser will understand in a response. (HTTP1.1 clients technically must support it, but simpler ones may not. In HTTP1.0 it was optional)
  • Compressing small files is often not useful at all; trying to compress 500 or so bytes of output is rarely really worth the CPU time spent on it.


mod_deflate

mod_deflate is implemented as a transparent output filter.

It is likely to be installed, though not necessarily enabled. Check that there is a line like the following in your apache config:

LoadModule deflate_module /usr/lib/apache2/modules/mod_deflate.so
For this reason, it is also smart to generously use
<IfModule deflate_module>
.


Perhaps the simplest way to use is to apply to few specific mime types, such as:

AddOutputFilterByType DEFLATE text/plain text/css text/javascript 
AddOutputFilterByType DEFLATE text/html application/xml application/xhtml+xml

You could set these globally if you wish.

Note that to apache, PHP has its own type (application/x-httpd-php). You can add it, but then you should keep PHP's internal compression disabled.


The module listens to environment options like no-gzip and dont-vary. This allows 'enable globally, disable for specific things' logic:

SetOutputFilter DEFLATE
SetEnvIfNoCase Request_URI \.(?:png|jp2|jpe?g|jpeg?|gif)$  no-gzip dont-vary
SetEnvIfNoCase Request_URI \.(?:t?gz|bz2|zip|rar|7z|sit)$  no-gzip dont-vary
SetEnvIfNoCase Request_URI \.pdf$                          no-gzip dont-vary
#It's probably much more work than the previous example, unless you don't mind 
# your CPU compressing uncompressable data


Since apache can set environment form many tests, you can also use this behaviour to disable compression for IE - which you always want, and probably want to do in global apache config. It seems everyone copy-pastes from the apache documentation:

BrowserMatch ^Mozilla/4         gzip-only-text/html
BrowserMatch ^Mozilla/4\.0[678] no-gzip
BrowserMatch \bMSI[E]           !no-gzip !gzip-only-text/html
# The bracketed E there is a fix to a past apache parse bug. 
# If it's fixed in IE7, what're the new tests exactly?
 
# Tells proxies to cache separately for each browser
Header append Vary User-Agent   env=!dont-vary
# This varies everything for user-agent by default unless dont-vary is set,
# which you can set on content you know it won't matter, for example
# when you won't compress it.

Notes:

  • can be set in server, vhost, directory, and .htaccess
  • You can also tweak the compression ratio versus resources tradeoff -
    DeflateCompressionLevel value
    directive.
  • It seems some browsers have problems with compressed external javascript specifically when it is included from the body section of a document, not the head. Something to keep in mind (and (verify) and detail here).
  • You can get apache to log the compression rates, to see how much it's helping. See [1] or [2] for details

mod_gzip

This article/section is a stub — probably a pile of half-sorted notes and assertions some of which may well be wrong, and not verified as a whole. Feel free to add or refine.

(section very unfinished)

mod_gzip works in a similar way to mod_deflate

<IfModule mod_gzip.c> 
   mod_gzip_on  Yes      
   #Why?:
   mod_gzip_dechunk yes  
 
   #What to use it on: (example)
   mod_gzip_item_exclude file "\.css$"  
   mod_gzip_item_exclude file "\.js$"
   mod_gzip_item_include file \.htm$
   mod_gzip_item_include file \.html$
   mod_gzip_item_include mime ^text/.*
   mod_gzip_item_exclude file "\.wml$"
 </IfModule>

It has some extra features, such as checking for an already-compressed version (.gz on disk) when doing static file serving, and being more configurable.


(Semi-)Manually

PHP filter

The sections ebove apply to specific types of static files - well, depending on how they are configured. They can be used to handle PHP's output as well, but PHP's handling may be slightly smarter about output chunking.

Support for compression was added around 4.0.4. If zlib is not compiled in, PHP will ignore you silently.


There are two methods. One is:

#configuration level  (php.ini)
zlib.output_compression = On

The other is:

# Globally (php.ini)
output_handler = ob_gzhandler
 
# ...or per script 
# as early as you can; certainly before header output:
ob_start("ob_gzhandler"); # at the start of the file
of_flush(); # at the end
 
#There is one trick to make that simpler to do. You can add the following to htaccess:
php_value auto_prepend_file gz_start.php
php_value auto_append_file  gz_start.php
# and have those two files contain the start and end statement.
# This also allows you to write IE-fixing code in one place, 
# and more importantly the PHP bug of it not checking whether the 
# first method is enabled server-wide (if it is, it does compression
# twice, which breaks completely), and any other options, 
# like perhaps disabling implicit flusing


In both cases, you probably want to not set it globally, but set it in apache config or .htaccess, allowing you you to set it per directory (or even for specific scripts, using Files or FilesMatch):

php_flag zlib.output_compression On
# or alternatively set a specific buffer size (default is 4KB):
php_value zlib.output_compression 2048
#(Note that's _value, not _flag)
 
#and optionally:
php_value zlib.output_compression_level 3
#Default seems to be 6, which is relatively heavy on CPU. 3 is ligter and decent. 
# Even 1 will be noticable improvement.

Notes:

  • The documentation says you can use iniset to enable "zlib.output_compression", but this is true for few PHP versions, if any ay all. It is non-ideal in other ways: You can't iniset the _level (apparently)(verify).

Also, if a higher level setting caused a script to compress, you can disable compression with iniset, but it will still use output buffering - even when you set explicit flushing.


Writing gzip from your own code

Check whether you can:

Supporting browsers will send a header like:

Accept-Encoding: gzip
Accept-Encoding: gzip, deflate

Some old browsers, like netscape version 4, have bugs and effectively lie about what they support - you'll want to test for them and not send them compresed content.


Signal that you are:

When you decide to use one of the advertized methods of compression, tell the browser about it, probably using:

Content-Encoding: gzip

There is also a Transfer-Encoding. The difference is largely semantic; the idea seems to be that Content-Encoding signals the data is meant to be a .gz file, while Transfer-Encoding states it's just about transfer - such as compressing (static or dynamic) HTML to save bandwidth. ((verify) both are well supported)

In practice, Content-Encoding serves both purposes; there little difference other than choices the browser may make based on this -- but things such as 'whether to save or display' are usually controlled by headers like the Content-Type response header.


Do it:

This is mostly just doing what you are saying.

Note that
Content-Length
header should report the size of the compressed data.

Pseudocode (for gzip only):

if request.headers['Accept-Encoding'].contains('gzip'):
    gzip_data = gzip.compress(output_data)
    response.headers["Content-Encoding"] = 'gzip'
    response.headers["Content-Length"]   = length(gzip_data)
    response.write(gzip_buffer)
else:
    #serve headers and data as usual


Chunked output involves telling Transfer-Encoding: chunked (something all HTTP1.1 agents must support), then writing fairly self-contained chunks (but I'm not sure about the details, either without or with compression)


Server side

You can do a few other things on the server side

  • MPM juggling.
  • use mod_proxy to act as a local proxying cache to your own site.
  • use mod_file_cache to cache never-ever-changing file metadata and data in memory, reducing IO at file serving time. It never re-checks them until apache is restarted, or in the mmap option (not available on windows), until you send apache a HUP or USR1 signal(verify).


See also