Skip to content

Commit

Permalink
feat: Preload routes and repl (#1222)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
rschristian authored Jan 7, 2025
1 parent 8a93ca5 commit b4f3adc
Show file tree
Hide file tree
Showing 15 changed files with 94 additions and 61 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"node-html-parser": "^6.1.13",
"preact": "10.15.1",
"preact-custom-element": "^4.3.0",
"preact-iso": "^2.6.3",
"preact-iso": "^2.8.1",
"preact-markup": "^2.1.1",
"preact-render-to-string": "^6.4.1",
"prismjs": "^1.29.0",
Expand Down
8 changes: 6 additions & 2 deletions src/components/blog-overview/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import config from '../../config.json';
import { useLanguage, useTranslation } from '../../lib/i18n';
import { getRouteName } from '../header';
import { Time } from '../time';
import { prefetchContent } from '../../lib/use-resource';
import { prefetchContent } from '../../lib/use-content';
import { BlogPage } from '../routes.jsx';
import s from './style.module.css';

export default function BlogOverview() {
Expand All @@ -15,7 +16,10 @@ export default function BlogOverview() {
{config.blog.map(post => {
const name = getRouteName(post, lang);
const excerpt = post.excerpt[lang] || post.excerpt.en;
const onMouseOver = () => prefetchContent(post.path);
const onMouseOver = () => {
BlogPage.preload();
prefetchContent(post.path);
};

return (
<article class={s.post}>
Expand Down
17 changes: 15 additions & 2 deletions src/components/content-region/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,27 @@ import widgets from '../widgets';
import style from './style.module.css';
import { useTranslation } from '../../lib/i18n';
import { TocContext } from '../table-of-contents';
import { prefetchContent } from '../../lib/use-resource';
import { prefetchContent } from '../../lib/use-content';
import { preloadRepl } from '../../lib/use-repl';
import { Repl, TutorialPage } from '../routes';

const COMPONENTS = {
...widgets,
a(props) {
if (props.href && props.href.startsWith('/')) {
const url = new URL(props.href, location.origin);
props.onMouseOver = () => prefetchContent(url.pathname);

props.onMouseOver = () => {
if (props.href.startsWith('/repl?code')) {
Repl.preload();
preloadRepl();
} else if (props.href.startsWith('/tutorial')) {
TutorialPage.preload();
preloadRepl();
}

prefetchContent(url.pathname);
};
}

return <a {...props} />;
Expand Down
2 changes: 1 addition & 1 deletion src/components/controllers/repl-page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import style from './repl/style.module.css';
export default function ReplPage() {
const { query } = useRoute();

useContent('repl');
useContent('/repl');

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

Expand Down
10 changes: 2 additions & 8 deletions src/components/controllers/repl/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { textToBase64 } from './query-encode.js';
import { ErrorOverlay } from './error-overlay';
import { EXAMPLES, fetchExample } from './examples';
import { useStoredValue } from '../../../lib/localstorage';
import { useResource } from '../../../lib/use-resource';
import { useRepl } from '../../../lib/use-repl';
import { parseStackTrace } from './errors';
import style from './style.module.css';
import REPL_CSS from './examples/style.css?raw';
Expand All @@ -26,13 +26,7 @@ export function Repl({ code }) {
// TODO: Needs some work for prerendering to not cause pop-in
if (typeof window === 'undefined') return null;

/**
* @type {{ Runner: import('./runner').default, CodeEditor: import('../../code-editor').default }}
*/
const { Runner, CodeEditor } = useResource(() => Promise.all([
import('../../code-editor'),
import('./runner')
]).then(([CodeEditor, Runner]) => ({ CodeEditor: CodeEditor.default, Runner: Runner.default })), ['repl']);
const { CodeEditor, Runner } = useRepl();

const applyExample = (e) => {
const slug = e.target.value;
Expand Down
5 changes: 2 additions & 3 deletions src/components/controllers/tutorial-page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { useEffect } from 'preact/hooks';
import { Tutorial } from './tutorial';
import { SolutionProvider } from './tutorial/contexts';
import { NotFound } from './not-found';
import { useContent } from '../../lib/use-content';
import { prefetchContent } from '../../lib/use-resource.js';
import { useContent, prefetchContent } from '../../lib/use-content';
import { tutorialRoutes } from '../../lib/route-utils';

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

function TutorialLayout() {
const { path, params } = useRoute();
const { html, meta } = useContent(!params.step ? 'tutorial/index' : path);
const { html, meta } = useContent(!params.step ? '/tutorial/index' : path);

// Preload the next chapter
useEffect(() => {
Expand Down
10 changes: 2 additions & 8 deletions src/components/controllers/tutorial/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { TutorialContext, SolutionContext } from './contexts';
import { ErrorOverlay } from '../repl/error-overlay';
import { parseStackTrace } from '../repl/errors';
import cx from '../../../lib/cx';
import { useResource } from '../../../lib/use-resource';
import { useRepl } from '../../../lib/use-repl';
import { useLanguage } from '../../../lib/i18n';
import { Splitter } from '../../splitter';
import config from '../../../config.json';
Expand Down Expand Up @@ -61,13 +61,7 @@ export function Tutorial({ html, meta }) {
// TODO: Needs some work for prerendering to not cause pop-in
if (typeof window === 'undefined') return null;

/**
* @type {{ Runner: import('../repl/runner').default, CodeEditor: import('../../code-editor').default }}
*/
const { Runner, CodeEditor } = useResource(() => Promise.all([
import('../../code-editor'),
import('../repl/runner')
]).then(([CodeEditor, Runner]) => ({ CodeEditor: CodeEditor.default, Runner: Runner.default })), ['repl']);
const { CodeEditor, Runner } = useRepl();

useEffect(() => {
if (meta.tutorial?.initial && editorCode !== meta.tutorial.initial) {
Expand Down
18 changes: 16 additions & 2 deletions src/components/header/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import Corner from './corner';
import { useOverlayToggle } from '../../lib/toggle-overlay';
import { useLocation } from 'preact-iso';
import { useLanguage } from '../../lib/i18n';
import { prefetchContent } from '../../lib/use-resource';
import { prefetchContent } from '../../lib/use-content';
import { preloadRepl } from '../../lib/use-repl';
import { Repl, TutorialPage } from '../routes';

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

const onMouseOver = () => {
if (prefetchHref.startsWith('/repl')) {
Repl.preload();
preloadRepl();
} else if (prefetchHref.startsWith('/tutorial')) {
TutorialPage.preload();
preloadRepl();
}

prefetchContent(prefetchHref);
};

return (
<a
href={href}
onMouseOver={() => prefetchContent(prefetchHref)}
onMouseOver={onMouseOver}
{...props}
data-route={route}
{...homeProps}
Expand Down
1 change: 0 additions & 1 deletion src/components/header/style.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,6 @@
height: 56px;
min-width: 80px;
overflow: visible;
background: var(--color-brand);
padding-right: 0.5rem;

@media (max-width: /* --header-mobile-breakpoint */ 50rem) {
Expand Down
6 changes: 3 additions & 3 deletions src/components/routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { DocPage } from './controllers/doc-page';
import { NotFound } from './controllers/not-found';
import { navRoutes } from '../lib/route-utils';

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

// @ts-ignore
const routeChange = url => typeof ga === 'function' && ga('send', 'pageview', url);
Expand Down
2 changes: 1 addition & 1 deletion src/components/sidebar/sidebar-nav.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useRoute } from 'preact-iso';
import cx from '../../lib/cx';
import { prefetchContent } from '../../lib/use-resource';
import { prefetchContent } from '../../lib/use-content';
import style from './sidebar-nav.module.css';

/**
Expand Down
20 changes: 18 additions & 2 deletions src/lib/use-content.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
import { useEffect } from 'preact/hooks';

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

/**
* @param {string} path
* @returns {{ html: string, meta: any }}
*/
export function useContent(path) {
const { html, meta } = fetchContent(path);
const [lang] = useLanguage();
const { html, meta } = useResource(() => getContent([lang, path]), [lang, path]);
useTitle(meta.title);
useDescription(meta.description);

return { html, meta };
}

/**
* @param {string} path
*/
export function prefetchContent(path) {
const lang = document.documentElement.lang;
const fetch = () => getContent([lang, path]);

const cacheKey = createCacheKey(fetch, [lang, path]);
if (CACHE.has(cacheKey)) return;

setupCacheEntry(fetch, cacheKey);
}

/**
* Set `document.title`
* @param {string} title
Expand Down
23 changes: 23 additions & 0 deletions src/lib/use-repl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useResource, createCacheKey, setupCacheEntry, CACHE } from './use-resource.js';

const loadChunks = () => Promise.all([
import('../components/code-editor'),
import('../components/controllers/repl/runner')
]).then(([CodeEditor, Runner]) => ({ CodeEditor: CodeEditor.default, Runner: Runner.default }));

/**
* @returns {void}
*/
export function preloadRepl() {
const cacheKey = createCacheKey(loadChunks, ['repl']);
if (CACHE.has(cacheKey)) return;

setupCacheEntry(loadChunks, cacheKey);
}

/**
* @returns {{ CodeEditor: import('../components/code-editor').default, Runner: import('../components/controllers/repl/runner').default }}
*/
export function useRepl() {
return useResource(loadChunks, ['repl']);
}
29 changes: 3 additions & 26 deletions src/lib/use-resource.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { useEffect } from 'preact/hooks';

import { getContent } from './content.js';
import { useLanguage } from './i18n';

/**
* @typedef {Object} CacheEntry
* @property {Promise<any>} promise
Expand All @@ -12,28 +9,8 @@ import { useLanguage } from './i18n';
*/

/** @type {Map<string, CacheEntry>} */
const CACHE = new Map();
const createCacheKey = (fn, deps) => '' + fn + JSON.stringify(deps);

/**
* @param {string} path
*/
export function prefetchContent(path) {
const lang = document.documentElement.lang;
const cacheKey = createCacheKey(() => getContent([lang, path]), [lang, path]);
if (CACHE.has(cacheKey)) return;

setupCacheEntry(() => getContent([lang, path]), cacheKey);
}

/**
* @param {string} path
* @returns {{ html: string, meta: any }}
*/
export function fetchContent(path) {
const [lang] = useLanguage();
return useResource(() => getContent([lang, path]), [lang, path]);
}
export const CACHE = new Map();
export const createCacheKey = (fn, deps) => '' + fn + JSON.stringify(deps);

export function useResource(fn, deps) {
const cacheKey = createCacheKey(fn, deps);
Expand Down Expand Up @@ -64,7 +41,7 @@ export function useResource(fn, deps) {
* @param {string} cacheKey
* @returns {CacheEntry}
*/
function setupCacheEntry(fn, cacheKey) {
export function setupCacheEntry(fn, cacheKey) {
/** @type {CacheEntry} */
const state = { promise: fn(), status: 'pending', result: undefined, users: 0 };

Expand Down

0 comments on commit b4f3adc

Please sign in to comment.