Skip to content

Pass-through caching demo. #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions service-worker/pass-through-caching/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Service Worker Sample: Pass-through Caching
===
See https://googlechrome.github.io/samples/service-worker/pass-through-caching/index.html for a live demo.

Learn more at http://www.chromestatus.com/feature/6561526227927040
131 changes: 131 additions & 0 deletions service-worker/pass-through-caching/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<!doctype html>
<!--
Copyright 2014 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">

<meta name="description" content="Sample of pass-through caching with Service Workers.">

<meta name="viewport" content="width=device-width, initial-scale=1">

<title>Service Worker Sample: Pass-through Caching</title>

<!-- Add to homescreen for Chrome on Android -->
<meta name="mobile-web-app-capable" content="yes">
<link rel="icon" sizes="192x192" href="../../images/touch/chrome-touch-icon-192x192.png">

<!-- Add to homescreen for Safari on iOS -->
<!-- TODO: Replace PLACEHOLDER with feature name. -->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PLACEHOLDER seems to have been removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks for catching that.

<meta name="apple-mobile-web-app-title" content="Service Worker Sample: Pass-through Caching">

<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon-precomposed" href="../../images/apple-touch-icon-precomposed.png">

<!-- Tile icon for Win8 (144x144 + tile color) -->
<meta name="msapplication-TileImage" content="images/touch/ms-touch-icon-144x144-precomposed.png">
<meta name="msapplication-TileColor" content="#3372DF">

<link rel="icon" href="../../images/favicon.ico">

<link rel="stylesheet" href="../../styles/main.css">
</head>

<body>
<h1>Service Worker Sample: Pass-through Caching</h1>

<p>Available in <a href="http://www.chromestatus.com/feature/6561526227927040">Chrome 40+</a></p>

<p>
This sample demonstrates basic Service Worker registration, in conjunction with pass-through
caching. After the Service Worker starts controlling this page, the first time a specific
resource is requested, it's <code>fetch()</code>ed from the network and a copy of the response
is stored in the Service Worker's cache. All subsequent times that the same resource is
requested, it's returned directly from the cache.
</p>

<p>
<strong>Note:</strong> This is a very aggressive approach to caching, and might not be appropriate
if your web application makes requests for arbitrary URLs as part of its normal operation
(e.g. a RSS client or a news aggregator), as the cache could end up containing large responses
that might not end up ever being accessed. Other approaches, like selectively caching based on
response headers or only caching responses served from a specific domain, might be more
appropriate for those use cases.
</p>

<p>
Visit <code>chrome://inspect/#service-workers</code> and click on the "inspect" link below
the registered Service Worker to view logging statements for the various actions the
<code><a href="service-worker.js">service-worker.js</a></code> script is performing.
</p>

<!-- // [START code-block] -->
<div class="output">
<div id="status"></div>

<ul id="images" style="display: none">
<li><img src="http://www.chromium.org/_/rsrc/1302286216006/config/customLogo.gif"></li>
<li><img src="http://www.chromium.org/_/rsrc/1365117468642/chromium-projects/chrome-64.png"></li>
</ul>
</div>

<script>
function showImages() {
document.querySelector('#images').style.display = 'block';
}

if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./service-worker.js', {scope: './'}).then(
function() {
// Registration was successful. Now, check to see whether the Service Worker is controlling the page.
if (navigator.serviceWorker.controller) {
// If .controller is set, then this page is being actively controlled by the Service Worker.
document.querySelector('#status').textContent = 'The Service Worker is currently handling network operations. ' +
'If you reload the page, the images (and everything else) will be served from the Service Worker\'s cache.';

showImages();
} else {
// If .controller isn't set, then prompt the user to reload the page so that the Service Worker can take
// control. Until that happens, the Service Worker's fetch handler won't be used.
document.querySelector('#status').textContent = 'Please reload this page to allow the Service Worker to handle network operations.';
}
},
function(error) {
// Something went wrong during registration. The service-worker.js file
// might be unavailable or contain a syntax error.
document.querySelector('#status').textContent = error;
}
);
} else {
// The current browser doesn't support Service Workers.
var aElement = document.createElement('a');
aElement.href = 'http://www.chromium.org/blink/serviceworker/service-worker-faq';
aElement.textContent = 'Service Workers are not supported in the current browser.';
document.querySelector('#status').appendChild(aElement);
}
</script>
<!-- // [END code-block] -->

<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-53563471-1', 'auto');
ga('send', 'pageview');
</script>
<!-- Built with love using Web Starter Kit -->
</body>
</html>
55 changes: 55 additions & 0 deletions service-worker/pass-through-caching/service-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// This sample illustrates an aggressive approach to caching, in which every valid response is
// cached and every request is first checked against the cache.
// This may not be an appropriate approach if your web application makes requests for
// arbitrary URLs as part of its normal operation (e.g. a RSS client or a news aggregator),
// as the cache could end up containing large responses that might not end up ever being accessed.
// Other approaches, like selectively caching based on response headers or only caching
// responses served from a specific domain, might be more appropriate for those use cases.

self.addEventListener('fetch', function(event) {
console.log('Handling fetch event for', event.request.url);

event.respondWith(
caches.open('pass-through-caching-sample').then(function(cache) {
return cache.match(event.request).then(function(response) {
if (response) {
// If there is an entry in the cache for event.request, then response will be defined
// and we can just return it.
console.log(' Found response in cache:', response);

return response;
} else {
// Otherwise, if there is no entry in the cache for event.request, response will be
// undefined, and we need to fetch() the resource.
console.log(' No response for %s found in cache. About to fetch from network...', event.request.url);

return fetch(event.request.clone()).then(function(response) {
console.log(' Response for %s from network is: %O', event.request.url, response);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommended checking the response was valid AND that you got a response.status == 200. If either of those are broken, then bail.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would checking that the response is valid entail—just a null check (are there any cases in which fetch() resolves with null instead of rejecting?) or is there some specific property of response that should be checking? I see that there's a termination reason defined as part of the response object, but I don't actually see it mapped to a property of what gets retruned in the current Chrome implementation.

Additionally, since Chrome returns opaque filtered responses for non-CORS requests, it's possible that response.status == 0 is a legitimate, cachable response. So it's a tradeoff between never caching opaque responses or caching them unconditionally, and risking caching a transient error response.

That being said, I will at least check response.status >= 400, to avoid caching any non-opaque error responses.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was discussing this point with a colleague and we both ended up agreeing that caching only 200 and 0 responses would be the safest bet for a recommended sample.

It will save people a lot of pain should a bad response get cached.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I talked to @slightlyoff about this, and he was (somewhat?) surprised that opaque filtered responses always had a status of 0, and suggested that I file whatwg/fetch#14 to see whether that restriction could be relaxed in the fetch() spec.

I'll keep caching whenever response.status < 400 and make a point of specifically highlighting this tradeoff in the comments. As long as the fetch() spec stays the same, it'll be each developer's decision whether they want to err on the side of caching too much or too little.


if (response && response.status < 400) {
// This avoids caching responses that we know are errors (i.e. HTTP status code of 4xx or 5xx).
// One limitation is that, for non-CORS requests, we get back a filtered opaque response
// (https://fetch.spec.whatwg.org/#concept-filtered-response-opaque) which will always have a
// .status of 0, regardless of whether the underlying HTTP call was successful. Since we're
// blindly caching those opaque responses, we run the risk of caching a transient error response.
//
// We need to call .clone() on the response object to save a copy of it to the cache.
// (https://fetch.spec.whatwg.org/#dom-request-clone)
cache.put(event.request, response.clone());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Over time the cache could get quite plump. Is it worth noting that storage isn't unlimited and at least mention that you should have some strategy to get rid of old stuff?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that's very true.

What's the limitation on Cache API storage that we're planning on enforcing with Chrome 40? And is that limit enforced based on cache name, service worker registration, or something else?

I'd like to include some actual numbers based on the Chrome implementation, and will add the caveat that other browsers or future versions of Chrome might enforce different limits.

I'll also add something about how naive pass-through caching is appropriate for web apps that make a limited set of HTTP requests, but web apps that might end up making arbitrary HTTP requests (like, say, an RSS client or news client) shouldn't blindly pass-through cache everything and need to periodically prune their caches if they do cache aggressively.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naive pass-through caching is appropriate for web apps that make a limited set of HTTP requests

Although apps that make a limited set of requests should probably be caching them up front in the install step

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've filed #42 to track the fact that this sample and probably a few others will need to address cache maintenance/versioning. I'll make those changes across all the samples in a consistent fashion in a follow-up PR.

Agreed that in general, pre-fetching will be a best practice, and #32 will cover more selective caching scenarios. I can envision there being some use cases where blindly caching everything is appropriate, though I'll make it clear in the comments that developers need to clearly understand their use case.

}

// Return the original response object, which will be used to fulfill the resource request.
return response;
});
}
}).catch(function(error) {
// This catch() will handle exceptions that arise from the match() or fetch() operations.
// Note that a HTTP error response (e.g. 404) will NOT trigger an exception.
// It will return a normal response object that has the appropriate error code set.
console.error(' Pass-through caching failed:', error);

throw error;
});
})
);
});