Skip to content

perf: compiler performance optimization #9674

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 56 commits into from
Nov 25, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
21d322b
wip: setup
yyx990803 Nov 12, 2023
e610fdb
wip: strip xmlMode / htmlMode
yyx990803 Nov 12, 2023
ea08664
wip: remove unused options
yyx990803 Nov 12, 2023
16942a2
wip: port parser
yyx990803 Nov 13, 2023
081ca7e
wip: save
yyx990803 Nov 13, 2023
4529f9b
wip: refactor line / column generation
yyx990803 Nov 14, 2023
23448e0
wip: loc for elements
yyx990803 Nov 14, 2023
cb38786
wip: parse directives
yyx990803 Nov 14, 2023
2fbef69
wip: optimize condenseWhitespace
yyx990803 Nov 14, 2023
a8feccd
wip: parse directive in tokenizer
yyx990803 Nov 14, 2023
73373b9
wip: source location for props
yyx990803 Nov 15, 2023
fd50689
wip: modifiers
yyx990803 Nov 15, 2023
49db157
wip: remove foreignContext check
yyx990803 Nov 15, 2023
4306249
wip: remove htmlMode
yyx990803 Nov 15, 2023
dd02806
wip: check duplicated attributes
yyx990803 Nov 15, 2023
90a4db9
wip: parse interpolation
yyx990803 Nov 15, 2023
9d189bf
wip: pre tag handling
yyx990803 Nov 15, 2023
e1aaf97
wip: v-pre handling
yyx990803 Nov 15, 2023
08f5826
wip: refine element type
yyx990803 Nov 16, 2023
a28c650
wip: comments
yyx990803 Nov 16, 2023
a870aaf
wip: getting ready for textmode handling
yyx990803 Nov 16, 2023
ba5aa7a
perf: optimize makeMap
yyx990803 Nov 16, 2023
e54e0e1
wip: parseMode
yyx990803 Nov 16, 2023
04c0b33
chore: disable parser tests for now
yyx990803 Nov 16, 2023
d506584
perf: optimize away isBuiltInType
yyx990803 Nov 16, 2023
1f7c0ce
wip: parse mode
yyx990803 Nov 17, 2023
956beba
refactor: swap to new template parser
yyx990803 Nov 17, 2023
b7b310b
wip: pass all non-error parsing tests
yyx990803 Nov 17, 2023
eadf6f8
wip: more tests passing
yyx990803 Nov 18, 2023
a52b5f0
wip: pass all compiler-core tests
yyx990803 Nov 18, 2023
973de35
wip: tune perf
yyx990803 Nov 18, 2023
1021e33
wip: entities parsing in browser
yyx990803 Nov 18, 2023
47224c6
wip: pass more compiler-dom tests
yyx990803 Nov 19, 2023
10f7d10
feat(compiler-core): support specifying root namespace when parsing
yyx990803 Nov 19, 2023
1aec019
wip: pass all compiler-dom tests
yyx990803 Nov 19, 2023
13032e8
wip: pass all compiler-ssr tests
yyx990803 Nov 20, 2023
65831e7
wip: pass all compiler-sfc tests
yyx990803 Nov 20, 2023
cc90ee0
wip: decodeEntities test
yyx990803 Nov 20, 2023
6ea6477
wip: support reusing template ast from sfc descriptor
yyx990803 Nov 20, 2023
c88fa63
wip: treat template with preprocessor as plain text
yyx990803 Nov 20, 2023
4c78df0
refactor: fix v-bind no-exp shorthand for new parser
yyx990803 Nov 21, 2023
02e0205
refactor: better naming for baseCompile argument
yyx990803 Nov 21, 2023
63d92ff
wip: compiler-sfc should not attach ast on template with src import
yyx990803 Nov 21, 2023
94d1cf4
refactor: use more efficient walk for importUsageCheck
yyx990803 Nov 21, 2023
8f13ee6
wip: should parse sfc template with lang=html
yyx990803 Nov 21, 2023
66f0d0d
wip: force re-parse on reused sfc template ast
yyx990803 Nov 21, 2023
d29e8b5
wip: parse error tests
yyx990803 Nov 22, 2023
a3c1a08
wip: shorten some method names
yyx990803 Nov 22, 2023
bf95cf2
chore: document rationale for ignoring some parsing errors
yyx990803 Nov 22, 2023
7bdde97
wip: parser v2 compat
yyx990803 Nov 22, 2023
8c87718
wip: should not reuse AST when using custom compiler
yyx990803 Nov 22, 2023
d5c03b1
perf: optimize position cloning
yyx990803 Nov 23, 2023
c7a3d54
perf(compiler-sfc): remove magic-string trim on script
yyx990803 Nov 23, 2023
a95d76e
perf(codegen): optimize line / column calculation during codegen
yyx990803 Nov 23, 2023
8928473
perf(codegen): optimize source map generation
yyx990803 Nov 24, 2023
7b0cc28
chore: restructure parser files
yyx990803 Nov 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
wip: entities parsing in browser
  • Loading branch information
yyx990803 committed Nov 21, 2023
commit 1021e335b89b7c157638e5983b8fc0d636b25e38
3 changes: 2 additions & 1 deletion packages/compiler-core/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export interface ParserOptions
*/
whitespace?: 'preserve' | 'condense'
/**
* Only needed for DOM compilers
* Only used for DOM compilers that runs in the browser.
* In non-browser builds, this option is ignored.
*/
decodeEntities?: (rawText: string, asAttr: boolean) => string
/**
Expand Down
114 changes: 65 additions & 49 deletions packages/compiler-core/src/parser/Tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,20 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
*/

import { ElementNode, Position } from '../ast'

/**
* Note: entities is a non-browser-build-only dependency.
* In the browser, we use an HTML element to do the decoding.
* Make sure all imports from entities are only used in non-browser branches
* so that it can be properly treeshaken.
*/
import {
EntityDecoder,
DecodingMode,
htmlDecodeTree
htmlDecodeTree,
fromCodePoint
} from 'entities/lib/decode.js'
import { ElementNode, Position } from '../ast'

export const enum ParseMode {
BASE,
Expand Down Expand Up @@ -170,7 +178,7 @@ export enum QuoteType {

export interface Callbacks {
ontext(start: number, endIndex: number): void
ontextentity(codepoint: number, endIndex: number): void
ontextentity(char: string, endIndex: number): void

oninterpolation(start: number, endIndex: number): void

Expand All @@ -180,7 +188,7 @@ export interface Callbacks {
onclosetag(start: number, endIndex: number): void

onattribdata(start: number, endIndex: number): void
onattribentity(codepoint: number): void
onattribentity(char: string): void
onattribend(quote: QuoteType, endIndex: number): void
onattribname(start: number, endIndex: number): void
onattribnameend(endIndex: number): void
Expand Down Expand Up @@ -233,15 +241,17 @@ export default class Tokenizer {
/** Reocrd newline positions for fast line / column calculation */
private newlines: number[] = []

private readonly entityDecoder: EntityDecoder
private readonly entityDecoder?: EntityDecoder

constructor(
private readonly stack: ElementNode[],
private readonly cbs: Callbacks
) {
this.entityDecoder = new EntityDecoder(htmlDecodeTree, (cp, consumed) =>
this.emitCodePoint(cp, consumed)
)
if (!__BROWSER__) {
this.entityDecoder = new EntityDecoder(htmlDecodeTree, (cp, consumed) =>
this.emitCodePoint(cp, consumed)
)
}
}

public mode = ParseMode.BASE
Expand Down Expand Up @@ -290,7 +300,7 @@ export default class Tokenizer {
}
this.state = State.BeforeTagName
this.sectionStart = this.index
} else if (c === CharCodes.Amp) {
} else if (!__BROWSER__ && c === CharCodes.Amp) {
this.startEntity()
} else if (c === this.delimiterOpen[0]) {
this.state = State.InterpolationOpen
Expand Down Expand Up @@ -398,7 +408,7 @@ export default class Tokenizer {
!(this.mode === ParseMode.SFC && this.stack.length === 0))
) {
// We have to parse entities in <title> and <textarea> tags.
if (c === CharCodes.Amp) {
if (!__BROWSER__ && c === CharCodes.Amp) {
this.startEntity()
}
} else if (this.fastForwardTo(CharCodes.Lt)) {
Expand Down Expand Up @@ -702,15 +712,15 @@ export default class Tokenizer {
}
}
private handleInAttributeValue(c: number, quote: number) {
if (c === quote) {
if (c === quote || (__BROWSER__ && this.fastForwardTo(quote))) {
this.cbs.onattribdata(this.sectionStart, this.index)
this.sectionStart = -1
this.cbs.onattribend(
quote === CharCodes.DoubleQuote ? QuoteType.Double : QuoteType.Single,
this.index + 1
)
this.state = State.BeforeAttributeName
} else if (c === CharCodes.Amp) {
} else if (!__BROWSER__ && c === CharCodes.Amp) {
this.startEntity()
}
}
Expand All @@ -727,7 +737,7 @@ export default class Tokenizer {
this.cbs.onattribend(QuoteType.Unquoted, this.index)
this.state = State.BeforeAttributeName
this.stateBeforeAttributeName(c)
} else if (c === CharCodes.Amp) {
} else if (!__BROWSER__ && c === CharCodes.Amp) {
this.startEntity()
}
}
Expand Down Expand Up @@ -796,29 +806,33 @@ export default class Tokenizer {
}

private startEntity() {
this.baseState = this.state
this.state = State.InEntity
this.entityStart = this.index
this.entityDecoder.startEntity(
this.baseState === State.Text || this.baseState === State.InSpecialTag
? DecodingMode.Legacy
: DecodingMode.Attribute
)
if (!__BROWSER__) {
this.baseState = this.state
this.state = State.InEntity
this.entityStart = this.index
this.entityDecoder!.startEntity(
this.baseState === State.Text || this.baseState === State.InSpecialTag
? DecodingMode.Legacy
: DecodingMode.Attribute
)
}
}

private stateInEntity(): void {
const length = this.entityDecoder.write(this.buffer, this.index)
if (!__BROWSER__) {
const length = this.entityDecoder!.write(this.buffer, this.index)

// If `length` is positive, we are done with the entity.
if (length >= 0) {
this.state = this.baseState
// If `length` is positive, we are done with the entity.
if (length >= 0) {
this.state = this.baseState

if (length === 0) {
this.index = this.entityStart
if (length === 0) {
this.index = this.entityStart
}
} else {
// Mark buffer as consumed.
this.index = this.buffer.length - 1
}
} else {
// Mark buffer as consumed.
this.index = this.buffer.length - 1
}
}

Expand Down Expand Up @@ -1002,8 +1016,8 @@ export default class Tokenizer {
}

private finish() {
if (this.state === State.InEntity) {
this.entityDecoder.end()
if (!__BROWSER__ && this.state === State.InEntity) {
this.entityDecoder!.end()
this.state = this.baseState
}

Expand Down Expand Up @@ -1052,25 +1066,27 @@ export default class Tokenizer {
}

private emitCodePoint(cp: number, consumed: number): void {
if (
this.baseState !== State.Text &&
this.baseState !== State.InSpecialTag
) {
if (this.sectionStart < this.entityStart) {
this.cbs.onattribdata(this.sectionStart, this.entityStart)
}
this.sectionStart = this.entityStart + consumed
this.index = this.sectionStart - 1
if (!__BROWSER__) {
if (
this.baseState !== State.Text &&
this.baseState !== State.InSpecialTag
) {
if (this.sectionStart < this.entityStart) {
this.cbs.onattribdata(this.sectionStart, this.entityStart)
}
this.sectionStart = this.entityStart + consumed
this.index = this.sectionStart - 1

this.cbs.onattribentity(cp)
} else {
if (this.sectionStart < this.entityStart) {
this.cbs.ontext(this.sectionStart, this.entityStart)
}
this.sectionStart = this.entityStart + consumed
this.index = this.sectionStart - 1
this.cbs.onattribentity(fromCodePoint(cp))
} else {
if (this.sectionStart < this.entityStart) {
this.cbs.ontext(this.sectionStart, this.entityStart)
}
this.sectionStart = this.entityStart + consumed
this.index = this.sectionStart - 1

this.cbs.ontextentity(cp, this.sectionStart)
this.cbs.ontextentity(fromCodePoint(cp), this.sectionStart)
}
}
}
}
49 changes: 29 additions & 20 deletions packages/compiler-core/src/parser/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { fromCodePoint } from 'entities/lib/decode.js'
import {
AttributeNode,
ConstantTypes,
Expand Down Expand Up @@ -29,6 +28,7 @@ import { defaultOnError, defaultOnWarn } from '../errors'
import { forAliasRE, isCoreComponent } from '../utils'

type OptionalOptions =
| 'decodeEntities'
| 'whitespace'
| 'isNativeTag'
| 'isBuiltInComponent'
Expand All @@ -37,28 +37,13 @@ type OptionalOptions =
type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
Pick<ParserOptions, OptionalOptions>

// The default decoder only provides escapes for characters reserved as part of
// the template syntax, and is only used if the custom renderer did not provide
// a platform-specific decoder.
const decodeRE = /&(gt|lt|amp|apos|quot);/g
const decodeMap: Record<string, string> = {
gt: '>',
lt: '<',
amp: '&',
apos: "'",
quot: '"'
}

export const defaultParserOptions: MergedParserOptions = {
parseMode: 'base',
delimiters: [`{{`, `}}`],
getNamespace: () => Namespaces.HTML,
isVoidTag: NO,
isPreTag: NO,
isCustomElement: NO,
// TODO handle entities
decodeEntities: (rawText: string): string =>
rawText.replace(decodeRE, (_, p1) => decodeMap[p1]),
onError: defaultOnError,
onWarn: defaultOnWarn,
comments: __DEV__
Expand All @@ -84,8 +69,8 @@ const tokenizer = new Tokenizer(stack, {
onText(getSlice(start, end), start, end)
},

ontextentity(cp, end) {
onText(fromCodePoint(cp), end - 1, end)
ontextentity(char, end) {
onText(char, end - 1, end)
},

oninterpolation(start, end) {
Expand Down Expand Up @@ -242,8 +227,8 @@ const tokenizer = new Tokenizer(stack, {
currentAttrEndIndex = end
},

onattribentity(codepoint) {
currentAttrValue += fromCodePoint(codepoint)
onattribentity(char) {
currentAttrValue += char
},

onattribnameend(end) {
Expand All @@ -265,6 +250,13 @@ const tokenizer = new Tokenizer(stack, {
onattribend(quote, end) {
if (currentElement && currentProp) {
if (quote !== QuoteType.NoValue) {
if (__BROWSER__ && currentAttrValue.includes('&')) {
// TODO should not do this in <script> or <style>
currentAttrValue = currentOptions.decodeEntities!(
currentAttrValue,
true
)
}
if (currentProp.type === NodeTypes.ATTRIBUTE) {
// assign value

Expand Down Expand Up @@ -422,6 +414,10 @@ function closeCurrentTag(end: number) {
}

function onText(content: string, start: number, end: number) {
if (__BROWSER__ && content.includes('&')) {
// TODO do not do this in <script> or <style>
content = currentOptions.decodeEntities!(content, false)
}
const parent = getParent()
const lastNode = parent.children[parent.children.length - 1]
if (lastNode?.type === NodeTypes.TEXT) {
Expand Down Expand Up @@ -697,6 +693,19 @@ export function baseParse(input: string, options?: ParserOptions): RootNode {
currentInput = input
currentOptions = extend({}, defaultParserOptions, options)

if (__DEV__) {
if (!__BROWSER__ && currentOptions.decodeEntities) {
console.warn(
`[@vue/compiler-core] decodeEntities option is passed but will be ` +
`ignored in non-browser builds.`
)
} else if (__BROWSER__ && !currentOptions.decodeEntities) {
throw new Error(
`[@vue/compiler-core] decodeEntities option is required in browser builds.`
)
}
}

tokenizer.mode =
currentOptions.parseMode === 'html'
? ParseMode.HTML
Expand Down
Loading