perf: speed up parser a little bit

~5% in local benchmarks by avoiding regex in some places
perf-parse
Simon Holthausen 1 day ago
parent 6374a2a0db
commit 6c1df6b00a
No known key found for this signature in database

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: speed up parser a little bit

@ -10,25 +10,7 @@ import read_options from './read/options.js';
import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
import * as state from '../../state.js';
/** @param {number} cc */
function is_whitespace(cc) {
// fast path for common whitespace
if (cc === 32 || (cc <= 13 && cc >= 9)) return true;
// rare whitespace — \u00a0, \u1680, \u2000-\u200a, \u2028, \u2029, \u202f, \u205f, \u3000, \ufeff
if (cc < 160) return false;
return (
cc === 160 ||
cc === 5760 ||
(cc >= 8192 && cc <= 8202) ||
cc === 8232 ||
cc === 8233 ||
cc === 8239 ||
cc === 8287 ||
cc === 12288 ||
cc === 65279
);
}
import { is_whitespace } from './utils/whitespace.js';
const regex_lang_attribute =
/<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/g;
@ -278,8 +260,27 @@ export class Parser {
};
}
/** @param {string} delimiter */
read_until(delimiter) {
if (this.index >= this.template.length) {
if (this.loose) return '';
e.unexpected_eof(this.template.length);
}
const start = this.index;
const index = this.template.indexOf(delimiter, start);
if (index !== -1) {
this.index = index;
return this.template.slice(start, this.index);
}
this.index = this.template.length;
return this.template.slice(start);
}
/** @param {RegExp} pattern */
read_until(pattern) {
read_until_regex(pattern) {
if (this.index >= this.template.length) {
if (this.loose) return '';
e.unexpected_eof(this.template.length);
@ -302,6 +303,7 @@ export class Parser {
e.expected_whitespace(this.index);
}
this.index++;
this.allow_whitespace();
}

@ -22,7 +22,7 @@ const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module'];
*/
export function read_script(parser, start, attributes) {
const script_start = parser.index;
const data = parser.read_until(regex_closing_script_tag);
const data = parser.read_until_regex(regex_closing_script_tag);
if (parser.index >= parser.template.length) {
e.element_unclosed(parser.template.length, 'script');
}

@ -16,8 +16,6 @@ const REGEX_WHITESPACE_OR_COLON = /[\s:]/;
const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/y;
const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/;
const REGEX_UNICODE_SEQUENCE = /\\[0-9a-fA-F]{1,6}(\r\n|\s)?/y;
const REGEX_COMMENT_CLOSE = /\*\//;
const REGEX_HTML_COMMENT_CLOSE = /-->/;
/**
* @param {Parser} parser
@ -478,7 +476,7 @@ function read_block_item(parser) {
function read_declaration(parser) {
const start = parser.index;
const property = parser.read_until(REGEX_WHITESPACE_OR_COLON);
const property = parser.read_until_regex(REGEX_WHITESPACE_OR_COLON);
parser.allow_whitespace();
parser.eat(':');
let index = parser.index;
@ -661,7 +659,7 @@ function allow_comment_or_whitespace(parser, capture_comments = true) {
}
if (parser.eat('<!--')) {
parser.read_until(REGEX_HTML_COMMENT_CLOSE);
parser.read_until('-->');
parser.eat('-->', true);
}
@ -676,7 +674,7 @@ function allow_comment_or_whitespace(parser, capture_comments = true) {
function read_comment(parser) {
const start = parser.index;
parser.eat('/*', true);
const value = parser.read_until(REGEX_COMMENT_CLOSE);
const value = parser.read_until('*/');
parser.eat('*/', true);
const end = parser.index;

@ -15,13 +15,10 @@ import { get_attribute_expression, is_expression_attribute } from '../../../util
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js';
import { locator } from '../../../state.js';
import * as b from '#compiler/builders';
import { is_whitespace } from '../utils/whitespace.js';
const regex_invalid_unquoted_attribute_value = /(\/>|[\s"'=<>`])/y;
const regex_closing_textarea_tag = /<\/textarea(\s[^>]*)?>/iy;
const regex_closing_comment = /-->/;
const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/;
const regex_token_ending_character = /[\s=/>"']/;
const regex_starts_with_quote_characters = /["']/y;
const regex_attribute_value = /(?:"([^"]*)"|'([^'])*'|([^>\s]+))/y;
const regex_doctype_name = /^![a-zA-Z]+$/;
@ -67,7 +64,7 @@ export default function element(parser) {
let parent = parser.current();
if (parser.eat('!--')) {
const data = parser.read_until(regex_closing_comment);
const data = parser.read_until('-->');
parser.eat('-->', true);
parser.append({
@ -81,7 +78,7 @@ export default function element(parser) {
}
if (parser.eat('/')) {
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
const name = read_tag_name(parser);
parser.allow_whitespace();
parser.eat('>', true);
@ -137,7 +134,7 @@ export default function element(parser) {
return;
}
const tag = read_tag(parser, regex_whitespace_or_slash_or_closing_tag);
const tag = read_tag(parser);
if (tag.name.startsWith('svelte:') && !meta_tags.has(tag.name)) {
const bounds = { start: start + 1, end: start + 1 + tag.name.length };
@ -475,7 +472,7 @@ function parent_is_shadowroot_template(stack) {
function read_static_attribute(parser) {
const start = parser.index;
const tag = read_tag(parser, regex_token_ending_character);
const tag = read_tag(parser, true);
if (!tag.name) return null;
/** @type {true | Array<AST.Text | AST.ExpressionTag>} */
@ -607,7 +604,7 @@ function read_attribute(parser) {
}
}
const tag = read_tag(parser, regex_token_ending_character);
const tag = read_tag(parser, true);
if (!tag.name) return null;
@ -731,7 +728,7 @@ function read_comment(parser) {
const start = parser.index;
if (parser.eat('//')) {
const value = parser.read_until(/\n/);
const value = parser.read_until('\n');
const end = parser.index;
return {
@ -747,7 +744,7 @@ function read_comment(parser) {
}
if (parser.eat('/*')) {
const value = parser.read_until(/\*\//);
const value = parser.read_until('*/');
parser.eat('*/');
const end = parser.index;
@ -847,25 +844,21 @@ function read_attribute_value(parser) {
* @returns {any[]}
*/
function read_sequence(parser, done, location) {
/** @type {AST.Text} */
let current_chunk = {
start: parser.index,
end: -1,
type: 'Text',
raw: '',
data: ''
};
/** @type {Array<AST.Text | AST.ExpressionTag>} */
const chunks = [];
let chunk_start = parser.index;
/** @param {number} end */
function flush(end) {
if (end > current_chunk.start) {
current_chunk.raw = parser.template.slice(current_chunk.start, end);
current_chunk.data = decode_character_references(current_chunk.raw, true);
current_chunk.end = end;
chunks.push(current_chunk);
if (end > chunk_start) {
const raw = parser.template.slice(chunk_start, end);
chunks.push({
start: chunk_start,
end,
type: 'Text',
raw,
data: decode_character_references(raw, true)
});
}
}
@ -879,12 +872,14 @@ function read_sequence(parser, done, location) {
if (parser.match('#')) {
const index = parser.index - 1;
parser.eat('#');
const name = parser.read_until(/[^a-z]/);
// const name = parser.read_until_regex(/[^a-z]/);
const name = read_lowercase_name(parser);
e.block_invalid_placement(index, name, location);
} else if (parser.match('@')) {
const index = parser.index - 1;
parser.eat('@');
const name = parser.read_until(/[^a-z]/);
// const name = parser.read_until_regex(/[^a-z]/);
const name = read_lowercase_name(parser);
e.tag_invalid_placement(index, name, location);
}
@ -907,14 +902,7 @@ function read_sequence(parser, done, location) {
};
chunks.push(chunk);
current_chunk = {
start: parser.index,
end: -1,
type: 'Text',
raw: '',
data: ''
};
chunk_start = parser.index;
} else {
parser.index++;
}
@ -929,12 +917,36 @@ function read_sequence(parser, done, location) {
/**
* @param {Parser} parser
* @param {RegExp} regex
* @param {boolean} [attribute]
*/
function read_tag_name(parser, attribute = false) {
const start = parser.index;
if (start >= parser.template.length && !parser.loose) e.unexpected_eof(parser.template.length);
while (parser.index < parser.template.length) {
const cc = parser.template.charCodeAt(parser.index);
if (
is_whitespace(cc) ||
cc === 47 || // /
cc === 62 || // >
(attribute && (cc === 34 || cc === 39 || cc === 61)) // " ' =
) {
break;
}
parser.index += 1;
}
return parser.template.slice(start, parser.index);
}
/**
* @param {Parser} parser
* @param {boolean} [attribute]
* @returns {Identifier & { start: number, end: number, loc: SourceLocation }}
*/
function read_tag(parser, regex) {
function read_tag(parser, attribute = false) {
const start = parser.index;
const name = parser.read_until(regex);
const name = read_tag_name(parser, attribute);
const end = parser.index;
return {
@ -948,3 +960,15 @@ function read_tag(parser, regex) {
}
};
}
/** @param {Parser} parser */
function read_lowercase_name(parser) {
const start = parser.index;
while (parser.index < parser.template.length) {
const cc = parser.template.charCodeAt(parser.index);
// a-z
if (cc < 97 || cc > 122) break;
parser.index += 1;
}
return parser.template.slice(start, parser.index);
}

@ -38,6 +38,8 @@ const entity_pattern_attr_value = get_entity_pattern(true);
* @param {boolean} is_attribute_value
*/
export function decode_character_references(html, is_attribute_value) {
if (html.indexOf('&') === -1) return html; // fast path
const entity_pattern = is_attribute_value ? entity_pattern_attr_value : entity_pattern_content;
return html.replace(
entity_pattern,

@ -0,0 +1,18 @@
/** @param {number} cc */
export function is_whitespace(cc) {
// fast path for common whitespace
if (cc === 32 || (cc <= 13 && cc >= 9)) return true;
// rare whitespace — \u00a0, \u1680, \u2000-\u200a, \u2028, \u2029, \u202f, \u205f, \u3000, \ufeff
if (cc < 160) return false;
return (
cc === 160 ||
cc === 5760 ||
(cc >= 8192 && cc <= 8202) ||
cc === 8232 ||
cc === 8233 ||
cc === 8239 ||
cc === 8287 ||
cc === 12288 ||
cc === 65279
);
}

@ -33,10 +33,14 @@ export let component_name = '<unknown>';
export let source;
/**
* The source code split into lines (set by `set_source`)
* @type {string[]}
* The source code split into lines, initialized when a diagnostic needs a code frame
* @type {string[] | undefined}
*/
export let source_lines = [];
let source_lines;
export function get_source_lines() {
return (source_lines ??= source.split('\n'));
}
/**
* True if compiling with `dev: true`
@ -52,7 +56,7 @@ export let locator;
/** @param {string} value */
export function set_source(value) {
source = value;
source_lines = source.split('\n');
source_lines = undefined;
const l = getLocator(source, { offsetLine: 1 });
@ -141,7 +145,7 @@ export function reset(state) {
runes = false;
component_name = UNKNOWN_FILENAME;
source = '';
source_lines = [];
source_lines = undefined;
filename = (state.filename ?? UNKNOWN_FILENAME).replace(/\\/g, '/');
warning_filter = state.warning ?? (() => true);
warnings = [];

@ -15,7 +15,7 @@ function tabs_to_spaces(str) {
* @param {number} column
*/
function get_code_frame(line, column) {
const lines = state.source_lines;
const lines = state.get_source_lines();
const frame_start = Math.max(0, line - 2);
const frame_end = Math.min(line + 3, lines.length);
const digits = String(frame_end + 1).length;

Loading…
Cancel
Save