Skip to content
41 changes: 33 additions & 8 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import {
encodeEntities,
styleObjToCss,
UNSAFE_NAME,
NAMESPACE_REPLACE_REGEX,
HTML_LOWER_CASE,
HTML_ENUMERATED,
SVG_CAMEL_CASE,
createComponent,
setDirty,
Expand Down Expand Up @@ -139,7 +137,7 @@ export async function renderToStringAsync(vnode, context) {
if (options[ROOT]) options[ROOT](vnode, { [CHILDREN]: parent, nodeType: 1 });

try {
const rendered = await _renderToString(
let rendered = _renderToString(
vnode,
context || EMPTY_OBJ,
false,
Expand All @@ -149,6 +147,10 @@ export async function renderToStringAsync(vnode, context) {
undefined
);

if (typeof rendered !== 'string' && !isArray(rendered)) {
rendered = await rendered;
}

if (isArray(rendered)) {
let count = 0;
let resolved = rendered;
Expand Down Expand Up @@ -381,8 +383,8 @@ function _renderToString(
cctx = provider ? provider.props.value : contextType.__;
}

let isClassComponent =
type.prototype && typeof type.prototype.render == 'function';
let prototype = type.prototype;
let isClassComponent = prototype && typeof prototype.render == 'function';
if (isClassComponent) {
rendered = /**#__NOINLINE__**/ renderClassComponent(vnode, cctx);
component = vnode[COMPONENT];
Expand Down Expand Up @@ -661,12 +663,19 @@ function _renderToString(
break;

default: {
let namespaceLength;
if (UNSAFE_NAME.test(name)) {
continue;
} else if (NAMESPACE_REPLACE_REGEX.test(name)) {
name = name.replace(NAMESPACE_REPLACE_REGEX, '$1:$2').toLowerCase();
} else if (
(name[4] === '-' || HTML_ENUMERATED.has(name)) &&
name[0] === 'x' &&
(namespaceLength = getNamespaceLength(name)) !== 0
) {
name =
name.slice(0, namespaceLength) +
':' +
name.slice(namespaceLength).toLowerCase();
} else if (
(name[4] === '-' || name === 'draggable' || name === 'spellcheck') &&
v != null
) {
// serialize boolean aria-xyz or enumerated attribute values as strings
Expand Down Expand Up @@ -711,6 +720,8 @@ function _renderToString(
} else if (typeof children === 'string') {
// single text child
html = encodeEntities(children);
} else if (typeof children === 'number') {
html = children + EMPTY_STR;
} else if (children != null && children !== false && children !== true) {
// recurse into this element VNode's children
let childSvgMode =
Expand Down Expand Up @@ -774,3 +785,17 @@ function isSignal(x) {
'value' in x
);
}

function getNamespaceLength(name) {
let length = name[1] === 'l' || name[3] === 'n' ? 5 : 3;
let char = name.charCodeAt(length);
return char >= 65 &&
char <= 90 &&
(length === 3
? name.slice(0, 3) === 'xml'
: name[1] === 'l'
? name.slice(0, 5) === 'xlink'
: name.slice(0, 5) === 'xmlns')
? length
: 0;
}
28 changes: 12 additions & 16 deletions src/lib/chunked.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,10 @@ function getDocumentClosingTagsIndex(html) {
}

async function forkPromises(renderer) {
if (renderer.suspended.length > 0) {
const suspensions = [...renderer.suspended];
while (renderer.suspended.length > 0) {
const length = renderer.suspended.length;
await Promise.all(renderer.suspended.map((s) => s.promise));
renderer.suspended = renderer.suspended.filter(
(s) => !suspensions.includes(s)
);
await forkPromises(renderer);
renderer.suspended.splice(0, length);
}
}

Expand All @@ -85,16 +82,8 @@ function handleError(error, vnode, renderChild) {

const id = vnode.__v;
const found = this.suspended.find((x) => x.id === id);
const race = new Deferred();

const abortSignal = this.abortSignal;
if (abortSignal) {
// @ts-ignore 2554 - implicit undefined arg
if (abortSignal.aborted) race.resolve();
else abortSignal.addEventListener('abort', race.resolve);
}

const promise = error.then(
let promise = error.then(
() => {
if (abortSignal && abortSignal.aborted) return;
const child = renderChild(vnode.props.children, vnode);
Expand All @@ -104,11 +93,18 @@ function handleError(error, vnode, renderChild) {
// to attempt to recover during hydration
this.onError
);
if (abortSignal) {
const race = new Deferred();
// @ts-ignore 2554 - implicit undefined arg
if (abortSignal.aborted) race.resolve();
else abortSignal.addEventListener('abort', race.resolve);
promise = Promise.race([promise, race.promise]);
}

this.suspended.push({
id,
vnode,
promise: Promise.race([promise, race.promise])
promise
});

const fallback = renderChild(vnode.props.fallback);
Expand Down
19 changes: 14 additions & 5 deletions src/lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,25 @@ export function isDirty(component) {
return component[DIRTY] === true;
}

// DOM properties that should NOT have "px" added when numeric
const ENCODED_ENTITIES = /["&<]/;

/** @param {string} str */
export function encodeEntities(str) {
// Skip all work for strings with no entities needing encoding:
if (str.length === 0 || ENCODED_ENTITIES.test(str) === false) return str;
let i = 0;
if (str.length < 8) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 8?

@JoviDeCroock JoviDeCroock Jul 15, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried all numbers and larger than 8 seemed to be slower in V8 😂

Use a character-code loop for strings shorter than eight characters and retain native indexOf scans for longer strings. Escaped short strings continue encoding from the first entity found by the fast-path scan.

for (; i < str.length; i++) {
const char = str.charCodeAt(i);
if (char === 34 || char === 38 || char === 60) break;
}
if (i === str.length) return str;
} else if (
str.indexOf('"') === -1 &&
str.indexOf('&') === -1 &&
str.indexOf('<') === -1
) {
return str;
}

let last = 0,
i = 0,
out = '',
ch = '';

Expand Down
29 changes: 24 additions & 5 deletions src/pretty.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
createComponent,
UNSAFE_NAME,
VOID_ELEMENTS,
NAMESPACE_REPLACE_REGEX,
SVG_CAMEL_CASE,
HTML_ENUMERATED,
HTML_LOWER_CASE,
Expand Down Expand Up @@ -154,7 +153,6 @@ function _renderToStringPretty(
let rendered;

let c = (vnode.__c = createComponent(vnode, context));

let renderHook = options[RENDER];

if (
Expand Down Expand Up @@ -255,7 +253,8 @@ function _renderToStringPretty(

for (let i = 0; i < attrs.length; i++) {
let name = attrs[i],
v = props[name];
v = props[name],
namespaceLength;
if (name === 'children') {
propChildren = v;
continue;
Expand Down Expand Up @@ -285,8 +284,14 @@ function _renderToStringPretty(
name = 'accept-charset';
} else if (name === 'httpEquiv') {
name = 'http-equiv';
} else if (NAMESPACE_REPLACE_REGEX.test(name)) {
name = name.replace(NAMESPACE_REPLACE_REGEX, '$1:$2').toLowerCase();
} else if (
name[0] === 'x' &&
(namespaceLength = getNamespaceLength(name)) !== 0
) {
name =
name.slice(0, namespaceLength) +
':' +
name.slice(namespaceLength).toLowerCase();
} else if (
(name.at(4) === '-' || HTML_ENUMERATED.has(name)) &&
v != null
Expand Down Expand Up @@ -461,6 +466,20 @@ function _renderToStringPretty(
return s;
}

function getNamespaceLength(name) {
let length = name[1] === 'l' || name[3] === 'n' ? 5 : 3;
let char = name.charCodeAt(length);
return char >= 65 &&
char <= 90 &&
(length === 3
? name.slice(0, 3) === 'xml'
: name[1] === 'l'
? name.slice(0, 5) === 'xlink'
: name.slice(0, 5) === 'xmlns')
? length
: 0;
}

function getComponentName(component) {
return (
component.displayName ||
Expand Down
Loading