Skip to content

Commit b4f3adc

Browse files
authored
feat: Preload routes and repl (#1222)
* feat: Preload split routes & REPL/Editor chunks * refactor: Fix tiny overlap of search bar on header * chore: Remove inconsistent use of extensions * chore: Remove leftover code * fix: Cache misses in repl & tutorial content fetch
1 parent 8a93ca5 commit b4f3adc

15 files changed

+94
-61
lines changed

package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
"node-html-parser": "^6.1.13",
8585
"preact": "10.15.1",
8686
"preact-custom-element": "^4.3.0",
87-
"preact-iso": "^2.6.3",
87+
"preact-iso": "^2.8.1",
8888
"preact-markup": "^2.1.1",
8989
"preact-render-to-string": "^6.4.1",
9090
"prismjs": "^1.29.0",

src/components/blog-overview/index.jsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import config from '../../config.json';
22
import { useLanguage, useTranslation } from '../../lib/i18n';
33
import { getRouteName } from '../header';
44
import { Time } from '../time';
5-
import { prefetchContent } from '../../lib/use-resource';
5+
import { prefetchContent } from '../../lib/use-content';
6+
import { BlogPage } from '../routes.jsx';
67
import s from './style.module.css';
78

89
export default function BlogOverview() {
@@ -15,7 +16,10 @@ export default function BlogOverview() {
1516
{config.blog.map(post => {
1617
const name = getRouteName(post, lang);
1718
const excerpt = post.excerpt[lang] || post.excerpt.en;
18-
const onMouseOver = () => prefetchContent(post.path);
19+
const onMouseOver = () => {
20+
BlogPage.preload();
21+
prefetchContent(post.path);
22+
};
1923

2024
return (
2125
<article class={s.post}>

src/components/content-region/index.jsx

+15-2
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,27 @@ import widgets from '../widgets';
44
import style from './style.module.css';
55
import { useTranslation } from '../../lib/i18n';
66
import { TocContext } from '../table-of-contents';
7-
import { prefetchContent } from '../../lib/use-resource';
7+
import { prefetchContent } from '../../lib/use-content';
8+
import { preloadRepl } from '../../lib/use-repl';
9+
import { Repl, TutorialPage } from '../routes';
810

911
const COMPONENTS = {
1012
...widgets,
1113
a(props) {
1214
if (props.href && props.href.startsWith('/')) {
1315
const url = new URL(props.href, location.origin);
14-
props.onMouseOver = () => prefetchContent(url.pathname);
16+
17+
props.onMouseOver = () => {
18+
if (props.href.startsWith('/repl?code')) {
19+
Repl.preload();
20+
preloadRepl();
21+
} else if (props.href.startsWith('/tutorial')) {
22+
TutorialPage.preload();
23+
preloadRepl();
24+
}
25+
26+
prefetchContent(url.pathname);
27+
};
1528
}
1629

1730
return <a {...props} />;

src/components/controllers/repl-page.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import style from './repl/style.module.css';
1010
export default function ReplPage() {
1111
const { query } = useRoute();
1212

13-
useContent('repl');
13+
useContent('/repl');
1414

1515
const code = useResource(() => getInitialCode(query), [query]);
1616

src/components/controllers/repl/index.jsx

+2-8
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { textToBase64 } from './query-encode.js';
55
import { ErrorOverlay } from './error-overlay';
66
import { EXAMPLES, fetchExample } from './examples';
77
import { useStoredValue } from '../../../lib/localstorage';
8-
import { useResource } from '../../../lib/use-resource';
8+
import { useRepl } from '../../../lib/use-repl';
99
import { parseStackTrace } from './errors';
1010
import style from './style.module.css';
1111
import REPL_CSS from './examples/style.css?raw';
@@ -26,13 +26,7 @@ export function Repl({ code }) {
2626
// TODO: Needs some work for prerendering to not cause pop-in
2727
if (typeof window === 'undefined') return null;
2828

29-
/**
30-
* @type {{ Runner: import('./runner').default, CodeEditor: import('../../code-editor').default }}
31-
*/
32-
const { Runner, CodeEditor } = useResource(() => Promise.all([
33-
import('../../code-editor'),
34-
import('./runner')
35-
]).then(([CodeEditor, Runner]) => ({ CodeEditor: CodeEditor.default, Runner: Runner.default })), ['repl']);
29+
const { CodeEditor, Runner } = useRepl();
3630

3731
const applyExample = (e) => {
3832
const slug = e.target.value;

src/components/controllers/tutorial-page.jsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import { useEffect } from 'preact/hooks';
33
import { Tutorial } from './tutorial';
44
import { SolutionProvider } from './tutorial/contexts';
55
import { NotFound } from './not-found';
6-
import { useContent } from '../../lib/use-content';
7-
import { prefetchContent } from '../../lib/use-resource.js';
6+
import { useContent, prefetchContent } from '../../lib/use-content';
87
import { tutorialRoutes } from '../../lib/route-utils';
98

109
import style from './tutorial/style.module.css';
@@ -22,7 +21,7 @@ export default function TutorialPage() {
2221

2322
function TutorialLayout() {
2423
const { path, params } = useRoute();
25-
const { html, meta } = useContent(!params.step ? 'tutorial/index' : path);
24+
const { html, meta } = useContent(!params.step ? '/tutorial/index' : path);
2625

2726
// Preload the next chapter
2827
useEffect(() => {

src/components/controllers/tutorial/index.jsx

+2-8
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { TutorialContext, SolutionContext } from './contexts';
1313
import { ErrorOverlay } from '../repl/error-overlay';
1414
import { parseStackTrace } from '../repl/errors';
1515
import cx from '../../../lib/cx';
16-
import { useResource } from '../../../lib/use-resource';
16+
import { useRepl } from '../../../lib/use-repl';
1717
import { useLanguage } from '../../../lib/i18n';
1818
import { Splitter } from '../../splitter';
1919
import config from '../../../config.json';
@@ -61,13 +61,7 @@ export function Tutorial({ html, meta }) {
6161
// TODO: Needs some work for prerendering to not cause pop-in
6262
if (typeof window === 'undefined') return null;
6363

64-
/**
65-
* @type {{ Runner: import('../repl/runner').default, CodeEditor: import('../../code-editor').default }}
66-
*/
67-
const { Runner, CodeEditor } = useResource(() => Promise.all([
68-
import('../../code-editor'),
69-
import('../repl/runner')
70-
]).then(([CodeEditor, Runner]) => ({ CodeEditor: CodeEditor.default, Runner: Runner.default })), ['repl']);
64+
const { CodeEditor, Runner } = useRepl();
7165

7266
useEffect(() => {
7367
if (meta.tutorial?.initial && editorCode !== meta.tutorial.initial) {

src/components/header/index.jsx

+16-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import Corner from './corner';
1010
import { useOverlayToggle } from '../../lib/toggle-overlay';
1111
import { useLocation } from 'preact-iso';
1212
import { useLanguage } from '../../lib/i18n';
13-
import { prefetchContent } from '../../lib/use-resource';
13+
import { prefetchContent } from '../../lib/use-content';
14+
import { preloadRepl } from '../../lib/use-repl';
15+
import { Repl, TutorialPage } from '../routes';
1416

1517
const LINK_FLAIR = {
1618
logo: InvertedLogo
@@ -220,10 +222,22 @@ const NavLink = ({ to, isOpen, route, ...props }) => {
220222
? { onContextMenu: BrandingRedirect, 'aria-label': 'Home' }
221223
: {};
222224

225+
const onMouseOver = () => {
226+
if (prefetchHref.startsWith('/repl')) {
227+
Repl.preload();
228+
preloadRepl();
229+
} else if (prefetchHref.startsWith('/tutorial')) {
230+
TutorialPage.preload();
231+
preloadRepl();
232+
}
233+
234+
prefetchContent(prefetchHref);
235+
};
236+
223237
return (
224238
<a
225239
href={href}
226-
onMouseOver={() => prefetchContent(prefetchHref)}
240+
onMouseOver={onMouseOver}
227241
{...props}
228242
data-route={route}
229243
{...homeProps}

src/components/header/style.module.css

-1
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,6 @@
470470
height: 56px;
471471
min-width: 80px;
472472
overflow: visible;
473-
background: var(--color-brand);
474473
padding-right: 0.5rem;
475474

476475
@media (max-width: /* --header-mobile-breakpoint */ 50rem) {

src/components/routes.jsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { DocPage } from './controllers/doc-page';
55
import { NotFound } from './controllers/not-found';
66
import { navRoutes } from '../lib/route-utils';
77

8-
const Repl = lazy(() => import('./controllers/repl-page'));
9-
const BlogPage = lazy(() => import('./controllers/blog-page'));
10-
const TutorialPage = lazy(() => import('./controllers/tutorial-page'));
8+
export const Repl = lazy(() => import('./controllers/repl-page'));
9+
export const BlogPage = lazy(() => import('./controllers/blog-page'));
10+
export const TutorialPage = lazy(() => import('./controllers/tutorial-page'));
1111

1212
// @ts-ignore
1313
const routeChange = url => typeof ga === 'function' && ga('send', 'pageview', url);

src/components/sidebar/sidebar-nav.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useRoute } from 'preact-iso';
22
import cx from '../../lib/cx';
3-
import { prefetchContent } from '../../lib/use-resource';
3+
import { prefetchContent } from '../../lib/use-content';
44
import style from './sidebar-nav.module.css';
55

66
/**

src/lib/use-content.js

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,36 @@
11
import { useEffect } from 'preact/hooks';
22

33
import { createTitle } from './page-title';
4-
import { fetchContent } from './use-resource.js';
4+
import { getContent } from './content.js';
5+
import { useLanguage } from './i18n';
6+
import { useResource, createCacheKey, setupCacheEntry, CACHE } from './use-resource.js';
57

68
/**
79
* @param {string} path
810
* @returns {{ html: string, meta: any }}
911
*/
1012
export function useContent(path) {
11-
const { html, meta } = fetchContent(path);
13+
const [lang] = useLanguage();
14+
const { html, meta } = useResource(() => getContent([lang, path]), [lang, path]);
1215
useTitle(meta.title);
1316
useDescription(meta.description);
1417

1518
return { html, meta };
1619
}
1720

21+
/**
22+
* @param {string} path
23+
*/
24+
export function prefetchContent(path) {
25+
const lang = document.documentElement.lang;
26+
const fetch = () => getContent([lang, path]);
27+
28+
const cacheKey = createCacheKey(fetch, [lang, path]);
29+
if (CACHE.has(cacheKey)) return;
30+
31+
setupCacheEntry(fetch, cacheKey);
32+
}
33+
1834
/**
1935
* Set `document.title`
2036
* @param {string} title

src/lib/use-repl.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useResource, createCacheKey, setupCacheEntry, CACHE } from './use-resource.js';
2+
3+
const loadChunks = () => Promise.all([
4+
import('../components/code-editor'),
5+
import('../components/controllers/repl/runner')
6+
]).then(([CodeEditor, Runner]) => ({ CodeEditor: CodeEditor.default, Runner: Runner.default }));
7+
8+
/**
9+
* @returns {void}
10+
*/
11+
export function preloadRepl() {
12+
const cacheKey = createCacheKey(loadChunks, ['repl']);
13+
if (CACHE.has(cacheKey)) return;
14+
15+
setupCacheEntry(loadChunks, cacheKey);
16+
}
17+
18+
/**
19+
* @returns {{ CodeEditor: import('../components/code-editor').default, Runner: import('../components/controllers/repl/runner').default }}
20+
*/
21+
export function useRepl() {
22+
return useResource(loadChunks, ['repl']);
23+
}

src/lib/use-resource.js

+3-26
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { useEffect } from 'preact/hooks';
22

3-
import { getContent } from './content.js';
4-
import { useLanguage } from './i18n';
5-
63
/**
74
* @typedef {Object} CacheEntry
85
* @property {Promise<any>} promise
@@ -12,28 +9,8 @@ import { useLanguage } from './i18n';
129
*/
1310

1411
/** @type {Map<string, CacheEntry>} */
15-
const CACHE = new Map();
16-
const createCacheKey = (fn, deps) => '' + fn + JSON.stringify(deps);
17-
18-
/**
19-
* @param {string} path
20-
*/
21-
export function prefetchContent(path) {
22-
const lang = document.documentElement.lang;
23-
const cacheKey = createCacheKey(() => getContent([lang, path]), [lang, path]);
24-
if (CACHE.has(cacheKey)) return;
25-
26-
setupCacheEntry(() => getContent([lang, path]), cacheKey);
27-
}
28-
29-
/**
30-
* @param {string} path
31-
* @returns {{ html: string, meta: any }}
32-
*/
33-
export function fetchContent(path) {
34-
const [lang] = useLanguage();
35-
return useResource(() => getContent([lang, path]), [lang, path]);
36-
}
12+
export const CACHE = new Map();
13+
export const createCacheKey = (fn, deps) => '' + fn + JSON.stringify(deps);
3714

3815
export function useResource(fn, deps) {
3916
const cacheKey = createCacheKey(fn, deps);
@@ -64,7 +41,7 @@ export function useResource(fn, deps) {
6441
* @param {string} cacheKey
6542
* @returns {CacheEntry}
6643
*/
67-
function setupCacheEntry(fn, cacheKey) {
44+
export function setupCacheEntry(fn, cacheKey) {
6845
/** @type {CacheEntry} */
6946
const state = { promise: fn(), status: 'pending', result: undefined, users: 0 };
7047

0 commit comments

Comments
 (0)