Unverified Commit 2d26cbb2 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] reduce initial cache size on web (#56103)

parent 960b7571
......@@ -357,7 +357,8 @@ class WebServiceWorker extends Target {
final Map<String, String> urlToHash = <String, String>{};
for (final File file in contents) {
// Do not force caching of source maps.
if (file.path.endsWith('main.dart.js.map')) {
if (file.path.endsWith('main.dart.js.map') ||
file.path.endsWith('.part.js.map')) {
continue;
}
final String url = globals.fs.path.toUri(
......@@ -376,7 +377,15 @@ class WebServiceWorker extends Target {
final File serviceWorkerFile = environment.outputDir
.childFile('flutter_service_worker.js');
final Depfile depfile = Depfile(contents, <File>[serviceWorkerFile]);
final String serviceWorker = generateServiceWorker(urlToHash);
final String serviceWorker = generateServiceWorker(urlToHash, <String>[
'main.dart.js',
'/',
'index.html',
'assets/LICENSE',
'assets/AssetManifest.json',
if (urlToHash.containsKey('assets/FontManifest.json'))
'assets/FontManifest.json',
]);
serviceWorkerFile
.writeAsStringSync(serviceWorker);
final DepfileService depfileService = DepfileService(
......@@ -393,40 +402,113 @@ class WebServiceWorker extends Target {
/// Generate a service worker with an app-specific cache name a map of
/// resource files.
///
/// We embed file hashes directly into the worker so that the byte for byte
/// The tool embeds file hashes directly into the worker so that the byte for byte
/// invalidation will automatically reactivate workers whenever a new
/// version is deployed.
// TODO(jonahwilliams): on re-activate, only evict stale assets.
String generateServiceWorker(Map<String, String> resources) {
String generateServiceWorker(Map<String, String> resources, List<String> coreBundle) {
return '''
'use strict';
const MANIFEST = 'flutter-app-manifest';
const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {
${resources.entries.map((MapEntry<String, String> entry) => '"${entry.key}": "${entry.value}"').join(",\n")}
};
self.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys().then(function (cacheName) {
return caches.delete(cacheName);
}).then(function (_) {
return caches.open(CACHE_NAME);
}).then(function (cache) {
return cache.addAll(Object.keys(RESOURCES));
// The application shell files that are downloaded before a service worker can
// start.
const CORE = [
${coreBundle.map((String file) => '"$file"').join(',\n')}];
// During install, the TEMP cache is populated with the application shell files.
self.addEventListener("install", (event) => {
return event.waitUntil(
caches.open(TEMP).then((cache) => {
return cache.addAll(CORE);
})
);
});
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request)
.then(function (response) {
if (response) {
return response;
// During activate, the cache is populated with the temp files downloaded in
// install. If this service worker is upgrading from one with a saved
// MANIFEST, then use this to retain unchanged resource files.
self.addEventListener("activate", function(event) {
return event.waitUntil(async function() {
try {
var contentCache = await caches.open(CACHE_NAME);
var tempCache = await caches.open(TEMP);
var manifestCache = await caches.open(MANIFEST);
var manifest = await manifestCache.match('manifest');
// When there is no prior manifest, clear the entire cache.
if (!manifest) {
await caches.delete(CACHE_NAME);
for (var request of await tempCache.keys()) {
var response = await tempCache.match(request);
await contentCache.put(request, response);
}
return fetch(event.request);
await caches.delete(TEMP);
// Save the manifest to make future upgrades efficient.
await manifestCache.put('manifest', new Response(JSON.stringify(RESOURCES)));
return;
}
var oldManifest = await manifest.json();
var origin = self.location.origin;
for (var request of await contentCache.keys()) {
var key = request.url.substring(origin.length + 1);
if (key == "") {
key = "/";
}
// If a resource from the old manifest is not in the new cache, or if
// the MD5 sum has changed, delete it. Otherwise the resource is left
// in the cache and can be reused by the new service worker.
if (!RESOURCES[key] || RESOURCES[key] != oldManifest[key]) {
await contentCache.delete(request);
}
}
// Populate the cache with the app shell TEMP files, potentially overwriting
// cache files preserved above.
for (var request of await tempCache.keys()) {
var response = await tempCache.match(request);
await contentCache.put(request, response);
}
await caches.delete(TEMP);
// Save the manifest to make future upgrades efficient.
await manifestCache.put('manifest', new Response(JSON.stringify(RESOURCES)));
return;
} catch (err) {
// On an unhandled exception the state of the cache cannot be guaranteed.
console.error('Failed to upgrade service worker: ' + err);
await caches.delete(CACHE_NAME);
await caches.delete(TEMP);
await caches.delete(MANIFEST);
}
}());
});
// The fetch handler redirects requests for RESOURCE files to the service
// worker cache.
self.addEventListener("fetch", (event) => {
var origin = self.location.origin;
var key = event.request.url.substring(origin.length + 1);
// If the URL is not the the RESOURCE list, skip the cache.
if (!RESOURCES[key]) {
return event.respondWith(fetch(event.request));
}
event.respondWith(caches.open(CACHE_NAME)
.then((cache) => {
return cache.match(event.request).then((response) => {
// Either respond with the cached resource, or perform a fetch and
// lazily populate the cache.
return response || fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
})
})
);
});
''';
}
......@@ -463,11 +463,17 @@ void main() {
}));
test('Generated service worker correctly inlines file hashes', () {
final String result = generateServiceWorker(<String, String>{'/foo': 'abcd'});
final String result = generateServiceWorker(<String, String>{'/foo': 'abcd'}, <String>[]);
expect(result, contains('{\n "/foo": "abcd"\n};'));
});
test('Generated service worker includes core files', () {
final String result = generateServiceWorker(<String, String>{'/foo': 'abcd'}, <String>['foo', 'bar']);
expect(result, contains('"foo",\n"bar"'));
});
test('WebServiceWorker generates a service_worker for a web resource folder', () => testbed.run(() async {
environment.outputDir.childDirectory('a').childFile('a.txt')
..createSync(recursive: true)
......@@ -497,6 +503,23 @@ void main() {
contains('"index.html": "d41d8cd98f00b204e9800998ecf8427e"'));
expect(environment.buildDir.childFile('service_worker.d'), exists);
}));
test('WebServiceWorker does not cache source maps', () => testbed.run(() async {
environment.outputDir
.childFile('main.dart.js')
.createSync(recursive: true);
environment.outputDir
.childFile('main.dart.js.map')
.createSync(recursive: true);
await const WebServiceWorker().build(environment);
// No caching of source maps.
expect(environment.outputDir.childFile('flutter_service_worker.js').readAsStringSync(),
isNot(contains('"main.dart.js.map"')));
// Expected twice, once for RESOURCES and once for CORE.
expect(environment.outputDir.childFile('flutter_service_worker.js').readAsStringSync(),
contains('"main.dart.js"'));
}));
}
class MockProcessManager extends Mock implements ProcessManager {}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment