Skip to content

Commit

Permalink
refactor(core): refactor SSG infrastructure (#10593)
Browse files Browse the repository at this point in the history
  • Loading branch information
slorber authored Oct 18, 2024
1 parent 14579cb commit c9f231a
Show file tree
Hide file tree
Showing 16 changed files with 356 additions and 309 deletions.
6 changes: 5 additions & 1 deletion packages/docusaurus-bundler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export {
} from './currentBundler';

export {getMinimizers} from './minification';
export {getHtmlMinifier, type HtmlMinifier} from './minifyHtml';
export {
getHtmlMinifier,
type HtmlMinifier,
type HtmlMinifierType,
} from './minifyHtml';
export {createJsLoaderFactory} from './loaders/jsLoader';
export {createStyleLoadersFactory} from './loaders/styleLoader';
18 changes: 5 additions & 13 deletions packages/docusaurus-bundler/src/minifyHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@

import {minify as terserHtmlMinifier} from 'html-minifier-terser';
import {importSwcHtmlMinifier} from './importFaster';
import type {DocusaurusConfig} from '@docusaurus/types';

// Historical env variable
const SkipHtmlMinification = process.env.SKIP_HTML_MINIFICATION === 'true';

export type HtmlMinifierType = 'swc' | 'terser';

export type HtmlMinifierResult = {
code: string;
warnings: string[];
Expand All @@ -25,24 +26,15 @@ const NoopMinifier: HtmlMinifier = {
minify: async (html: string) => ({code: html, warnings: []}),
};

type SiteConfigSlice = {
future: {
experimental_faster: Pick<
DocusaurusConfig['future']['experimental_faster'],
'swcHtmlMinimizer'
>;
};
};

export async function getHtmlMinifier({
siteConfig,
type,
}: {
siteConfig: SiteConfigSlice;
type: HtmlMinifierType;
}): Promise<HtmlMinifier> {
if (SkipHtmlMinification) {
return NoopMinifier;
}
if (siteConfig.future.experimental_faster.swcHtmlMinimizer) {
if (type === 'swc') {
return getSwcMinifier();
} else {
return getTerserMinifier();
Expand Down
69 changes: 48 additions & 21 deletions packages/docusaurus-logger/src/perfLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ import logger from './logger';

// For now this is a private env variable we use internally
// But we'll want to expose this feature officially some day
const PerfDebuggingEnabled: boolean = !!process.env.DOCUSAURUS_PERF_LOGGER;
const PerfDebuggingEnabled: boolean =
process.env.DOCUSAURUS_PERF_LOGGER === 'true';

const Thresholds = {
min: 5,
yellow: 100,
red: 1000,
};

const PerfPrefix = logger.yellow(`[PERF] `);
const PerfPrefix = logger.yellow(`[PERF]`);

// This is what enables to "see the parent stack" for each log
// Parent1 > Parent2 > Parent3 > child trace
Expand All @@ -42,6 +43,14 @@ type Memory = {
after: NodeJS.MemoryUsage;
};

function getMemory(): NodeJS.MemoryUsage {
// Before reading memory stats, we explicitly call the GC
// Note: this only works when Node.js option "--expose-gc" is provided
globalThis.gc?.();

return process.memoryUsage();
}

function createPerfLogger(): PerfLoggerAPI {
if (!PerfDebuggingEnabled) {
const noop = () => {};
Expand Down Expand Up @@ -73,29 +82,35 @@ function createPerfLogger(): PerfLoggerAPI {
);
};

const formatStatus = (error: Error | undefined): string => {
return error ? logger.red('[KO]') : ''; // logger.green('[OK]');
};

const printPerfLog = ({
label,
duration,
memory,
error,
}: {
label: string;
duration: number;
memory: Memory;
error: Error | undefined;
}) => {
if (duration < Thresholds.min) {
return;
}
console.log(
`${PerfPrefix + label} - ${formatDuration(duration)} - ${formatMemory(
memory,
)}`,
`${PerfPrefix}${formatStatus(error)} ${label} - ${formatDuration(
duration,
)} - ${formatMemory(memory)}`,
);
};

const start: PerfLoggerAPI['start'] = (label) =>
performance.mark(label, {
detail: {
memoryUsage: process.memoryUsage(),
memoryUsage: getMemory(),
},
});

Expand All @@ -110,30 +125,42 @@ function createPerfLogger(): PerfLoggerAPI {
duration,
memory: {
before: memoryUsage,
after: process.memoryUsage(),
after: getMemory(),
},
error: undefined,
});
};

const log: PerfLoggerAPI['log'] = (label: string) =>
console.log(PerfPrefix + applyParentPrefix(label));
console.log(`${PerfPrefix} ${applyParentPrefix(label)}`);

const async: PerfLoggerAPI['async'] = async (label, asyncFn) => {
const finalLabel = applyParentPrefix(label);
const before = performance.now();
const memoryBefore = process.memoryUsage();
const result = await ParentPrefix.run(finalLabel, () => asyncFn());
const memoryAfter = process.memoryUsage();
const duration = performance.now() - before;
printPerfLog({
label: finalLabel,
duration,
memory: {
before: memoryBefore,
after: memoryAfter,
},
});
return result;
const memoryBefore = getMemory();

const asyncEnd = ({error}: {error: Error | undefined}) => {
const memoryAfter = getMemory();
const duration = performance.now() - before;
printPerfLog({
error,
label: finalLabel,
duration,
memory: {
before: memoryBefore,
after: memoryAfter,
},
});
};

try {
const result = await ParentPrefix.run(finalLabel, () => asyncFn());
asyncEnd({error: undefined});
return result;
} catch (e) {
asyncEnd({error: e as Error});
throw e;
}
};

return {
Expand Down
1 change: 1 addition & 0 deletions packages/docusaurus-types/src/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ export type DocusaurusConfig = {
*
* @see https://docusaurus.io/docs/api/docusaurus-config#ssrTemplate
*/
// TODO Docusaurus v4 - rename to ssgTemplate?
ssrTemplate?: string;
/**
* Will be used as title delimiter in the generated `<title>` tag.
Expand Down
82 changes: 15 additions & 67 deletions packages/docusaurus/src/client/renderToHtml.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,74 +7,22 @@

import type {ReactNode} from 'react';
import {renderToPipeableStream} from 'react-dom/server';
import {Writable} from 'stream';
import {PassThrough} from 'node:stream';
import {text} from 'node:stream/consumers';

// See also https://github.com/facebook/react/issues/31134
// See also https://github.com/facebook/docusaurus/issues/9985#issuecomment-2396367797
export async function renderToHtml(app: ReactNode): Promise<string> {
// Inspired from
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
// https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/static-entry.js
const writableStream = new WritableAsPromise();

const {pipe} = renderToPipeableStream(app, {
onError(error) {
writableStream.destroy(error as Error);
},
onAllReady() {
pipe(writableStream);
},
});

return writableStream.getPromise();
}

// WritableAsPromise inspired by https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/server-utils/writable-as-promise.js

/* eslint-disable no-underscore-dangle */
class WritableAsPromise extends Writable {
private _output: string;
private _deferred: {
promise: Promise<string> | null;
resolve: (value: string) => void;
reject: (reason: Error) => void;
};

constructor() {
super();
this._output = ``;
this._deferred = {
promise: null,
resolve: () => null,
reject: () => null,
};
this._deferred.promise = new Promise((resolve, reject) => {
this._deferred.resolve = resolve;
this._deferred.reject = reject;
return new Promise((resolve, reject) => {
const passThrough = new PassThrough();
const {pipe} = renderToPipeableStream(app, {
onError(error) {
reject(error);
},
onAllReady() {
pipe(passThrough);
text(passThrough).then(resolve, reject);
},
});
}

override _write(
chunk: {toString: () => string},
_enc: unknown,
next: () => void,
) {
this._output += chunk.toString();
next();
}

override _destroy(error: Error | null, next: (error?: Error | null) => void) {
if (error instanceof Error) {
this._deferred.reject(error);
} else {
next();
}
}

override end() {
this._deferred.resolve(this._output);
return this.destroy();
}

getPromise(): Promise<string> {
return this._deferred.promise!;
}
});
}
3 changes: 3 additions & 0 deletions packages/docusaurus/src/client/serverEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ const render: AppRenderer = async ({pathname}) => {
const html = await renderToHtml(app);

const collectedData: PageCollectedData = {
// TODO Docusaurus v4 refactor: helmet state is non-serializable
// this makes it impossible to run SSG in a worker thread
helmet: (helmetContext as FilledContext).helmet,

anchors: statefulBrokenLinks.getCollectedAnchors(),
links: statefulBrokenLinks.getCollectedLinks(),
modules: Array.from(modules),
Expand Down
Loading

0 comments on commit c9f231a

Please sign in to comment.