Skip to content

Commit b9d3325

Browse files
authored
Prefetch and/or prerender pages when likely navigated to (#13296)
Implements a mechanism to specify pages to be prefetched and prerendered when likely navigated to, improving page loading speed and making website navigation feel more snappy and seamless. - If a page has a `next` (page) specified, prerender it. - This for example, makes navigating to the next lesson in the tutorial feel immediate. - If a page has `prev` (page) specified, prefetch it. - If on the homepage, prefetch the pages linked to in the top nav. - On any page, prefetch a page if hovering a link to it. - On any page, prerender a page if beginning to click a link to it. Note that the [Speculation Rules API](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API) this relies on mostly only works on Chromium-based browsers currently with WebKit having some initial support.
1 parent 1cc9064 commit b9d3325

4 files changed

Lines changed: 109 additions & 19 deletions

File tree

site/lib/src/extensions/registry.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import 'glossary_link_processor.dart';
1010
import 'header_extractor.dart';
1111
import 'header_processor.dart';
1212
import 'table_processor.dart';
13-
import 'tutorial_prefetch_processor.dart';
13+
import 'tutorial_navigation.dart';
1414
import 'tutorial_structure_processor.dart';
1515

1616
/// A list of all node-processing, page extensions to applied to

site/lib/src/extensions/tutorial_prefetch_processor.dart renamed to site/lib/src/extensions/tutorial_navigation.dart

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import 'package:jaspr/dom.dart';
6-
import 'package:jaspr/jaspr.dart';
75
import 'package:jaspr_content/jaspr_content.dart';
86

97
import '../models/tutorial_model.dart';
108

11-
/// A page extension for Jaspr Content that adds page navigation and a
12-
/// prefetch link for the next unit to the current tutorial page.
9+
/// A page extension for Jaspr Content that adds
10+
/// page navigation to the current tutorial page.
1311
final class TutorialNavigationExtension implements PageExtension {
1412
const TutorialNavigationExtension();
1513

@@ -67,19 +65,6 @@ final class TutorialNavigationExtension implements PageExtension {
6765
},
6866
);
6967

70-
if (nextChapter == null) {
71-
return nodes;
72-
}
73-
74-
return [
75-
ComponentNode(
76-
Document.head(
77-
children: [
78-
link(rel: 'prefetch', href: nextChapter.url),
79-
],
80-
),
81-
),
82-
...nodes,
83-
];
68+
return nodes;
8469
}
8570
}

site/lib/src/layouts/dash_layout.dart

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:convert';
6+
57
import 'package:jaspr/dom.dart';
68
import 'package:jaspr/jaspr.dart';
79
import 'package:jaspr_content/jaspr_content.dart';
@@ -25,6 +27,14 @@ abstract class FlutterDocsLayout extends PageLayoutBase {
2527

2628
String get defaultSidenav => 'default';
2729

30+
/// Returns page-specific URLs to eagerly speculate on, in addition to
31+
/// the document-level rules that match all internal links.
32+
///
33+
/// Override in subclasses to provide page-specific URLs for
34+
/// eager prerendering and prefetching.
35+
({Set<String> prerender, Set<String> prefetch}) speculationUrls(Page page) =>
36+
const (prerender: {}, prefetch: {});
37+
2838
@override
2939
@mustCallSuper
3040
Iterable<Component> buildHead(Page page) {
@@ -148,6 +158,9 @@ ga('create', 'UA-67589403-1', 'auto');
148158
ga('send', 'pageview');
149159
</script>
150160
'''),
161+
// Add speculation rules and prefetch fallback links for
162+
// URLs provided by subclass overrides of speculationUrls.
163+
..._buildSpeculationRulesHead(page),
151164
];
152165
}
153166

@@ -263,4 +276,66 @@ if (sidenav) {
263276
],
264277
);
265278
}
279+
280+
/// Builds the speculation rules `<script>` and `<link rel="prefetch">`
281+
/// fallback tags for the given [page].
282+
///
283+
/// Includes page-specific list rules from [speculationUrls] and
284+
/// document rules that prefetch internal links on hover (`moderate`)
285+
/// and prerender them on click (`conservative`).
286+
///
287+
/// Add the `no-prerender` class to a link to
288+
/// exclude it from document-level prerendering.
289+
List<Component> _buildSpeculationRulesHead(Page page) {
290+
final (:prerender, :prefetch) = speculationUrls(page);
291+
292+
// Exclude prerendered URLs from the prefetch list since
293+
// prerendering is a superset of prefetching.
294+
final prefetchOnly = prefetch.difference(prerender);
295+
296+
// Document rules to match same-origin links across the page.
297+
const internalLink = {'href_matches': '/*'};
298+
const notNoPrerender = {
299+
'not': {'selector_matches': '.no-prerender'},
300+
};
301+
302+
final rules = jsonEncode({
303+
'prefetch': [
304+
// Prefetch internal links on hover.
305+
{
306+
'where': internalLink,
307+
'eagerness': 'moderate',
308+
},
309+
// Prefetch specific URLs from the page eagerly.
310+
if (prefetchOnly.isNotEmpty)
311+
{
312+
'urls': [...prefetchOnly],
313+
},
314+
],
315+
'prerender': [
316+
// Prerender internal links on click,
317+
// unless the link has the 'no-prerender' class.
318+
{
319+
'where': {
320+
'and': [internalLink, notNoPrerender],
321+
},
322+
'eagerness': 'conservative',
323+
},
324+
// Prerender specific URLs from the page eagerly.
325+
if (prerender.isNotEmpty)
326+
{
327+
'urls': [...prerender],
328+
'eagerness': 'eager',
329+
},
330+
],
331+
}).replaceAll('</', r'<\/');
332+
333+
return [
334+
RawText('<script type="speculationrules">$rules</script>'),
335+
// Fall back to prefetch link tags for browsers without
336+
// Speculation Rules API support.
337+
for (final url in {...prerender, ...prefetch})
338+
link(rel: 'prefetch', href: url),
339+
];
340+
}
266341
}

site/lib/src/layouts/doc_layout.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,27 @@ class DocLayout extends FlutterDocsLayout {
2424

2525
bool get allowBreadcrumbs => true;
2626

27+
@override
28+
({Set<String> prerender, Set<String> prefetch}) speculationUrls(Page page) {
29+
// On the homepage, prefetch pages commonly navigated to,
30+
// such as entries from the top navigation menu.
31+
if (page.path == 'index.md') {
32+
return (
33+
prerender: const {},
34+
prefetch: const {
35+
'/learn/pathway',
36+
'/ai/create-with-ai',
37+
},
38+
);
39+
}
40+
41+
final pageData = page.data.page;
42+
return (
43+
prerender: {?_pathFromPageInfo(pageData['next'])},
44+
prefetch: {?_pathFromPageInfo(pageData['prev'])},
45+
);
46+
}
47+
2748
@override
2849
Component buildBody(Page page, Component child) {
2950
final pageData = page.data.page;
@@ -89,3 +110,12 @@ class DocLayout extends FlutterDocsLayout {
89110
);
90111
}
91112
}
113+
114+
/// Extracts and returns the `path` value from a page info map,
115+
/// or `null` if [data] is not a map or has no `path` entry.
116+
String? _pathFromPageInfo(Object? data) {
117+
if (data case {'path': final String path}) {
118+
return path;
119+
}
120+
return null;
121+
}

0 commit comments

Comments
 (0)