Static-tag intrinsic: `<element tag={tag}>` for elements whose tag is fixed at definition time

Aperta
#3,429 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
45/100
Tipo di issue
Funzionalità
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
babel, typescript

Direzione di ricerca

Inizia dagli entry point dei compilatori Babel e Oxc e dal lowering esistente di DOM, SSR e universal per i tag letterali e le radici spread. Segui i relativi test di parità dell’hydration e i typings di IntrinsicElements; il lavoro è completo quando la forma del tag viene validata e sottoposta a lowering in modo coerente, i tag mancanti generano un errore, il typing è strict e lo scenario di hydration passa su tutti i renderer.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

enhancement

Problem

Libraries that build components around a native element whose tag is fixed at definition time — styled.li, styled("button"), any design-system primitive factory — have no good way to render that element:

  • dynamic(() => tag) is the only isomorphic path from a tag string to an element. It pays a factory memo plus a per-instance memo (and a hydration id) for a value that never changes. In yak-bench's browser hydrate matrix, the lane that renders the styled element through dynamic() is 1.47× slower (geomean) than the same code with the element compiled from a literal tag; everything else in the two lanes is identical. One memo per element is the entire gap.
  • A runtime factory (element(tag) returning a component) removes the memos but can't take spreads natively: a use-site {...rest} has to be merged into a props object and crossed through a component boundary before spread() sees it, and static attributes are walked instead of compiled.
  • Hand-rolling it means an isServer fork over internals (getNextElement, runHydrationEvents, sharedConfig.hydrating vs ssrElement), which is what next-yak/next-yak#644 did.

Profiling the composition-heavy SSR cases (tabs) shows Solid's own rendering at ~7% of time and props plumbing (merge, omit, proxies, and the GC they generate) at ~55%; the styled-element seam is where most of that plumbing is introduced.

Proposal

A reserved intrinsic whose tag comes from an in-scope identifier:

function styled(tag) {
  return props => (
    <element tag={tag} {...filtered(props)} class={cls(props)}>
      {props.children}
    </element>
  );
}

The compiler lowers it exactly as it would a literal tag, with createElement(tag) in place of the template clone:

  • DOM: hydrating ? getNextElement() : createElement(tag); spread(el, [filtered, { get class() {…} }]) (the sources array from #3419/#3423); insert(el, () => props.children); runHydrationEvents() when hydrating. Two reactive nodes with children, one without, zero memos. Static child subtrees compile to their own templates and are appended; dynamic children go through insert.
  • SSR: ssrElement(tag, [filtered, { class }], () => props.children, true) — one walk (#3418), _hk on the element. Hydration ids stay aligned because both sides do exactly what a compiled spread root does today.
  • Universal: renderer createElement(tag) + spread.

The string element never reaches the DOM or the SSR output; the compiler consumes it. <element> without tag is a compile error, not a fall-through to a real tag.

Static rule

tag is an expression evaluated once, untracked, when the element is created — the same contract as <Provider value>. It is never reactive, and the compiler does not try to prove it constant (a binding in an enclosing scope can be reassigned; constancy isn't globally provable, so a scope rule would restrict real code while guaranteeing nothing). The lowering reads it exactly once (createElement(tag) / ssrElement(tag, …)); later changes to whatever it was read from are ignored, as with value.

  • <element> without tag is a compile error, not a fall-through to a real tag.
  • dynamic() / <Dynamic component> remain the answer for a tag that can change; the per-instance memo is the price of being dynamic. The two features stay vocabulary-distinct (as → Dynamic, tag → element), and the IntrinsicElements.element doc comment states the read-once rule so it shows on hover.

Why not <element:tag>

The namespaced form was considered first — it has the nice property that JSXNamespacedName can only hold a bare identifier, so props.as is syntactically impossible. Both parsers accept it and TS 6.0.3 types it through a template-literal index signature. But TS treats element:tag as the string "element:tag": the variable is never bound, so noUnusedLocals/noUnusedParameters flag tag (TS6133, verified), unbound identifiers aren't errors, and rename/go-to-definition are blind. That is exactly the use: directive experience. Putting the tag in an expression position (tag={tag}) gives TS the binding; the read-once rule is a documented contract, as it is for <Provider value>.

Naming

  • element: not in HTML, SVG, or MathML, nor in Solid's IntrinsicElements or either compiler's tag tables. The only history is the Web Components v0 declarative <element name="x-foo">, removed from the spec in August 2013 and never shipped unflagged. Custom elements can't claim it (hyphen required).
  • tag, not name: name is a real attribute on the elements most likely to be wrapped (input, button, select, textarea, form, iframe, …) and is routinely forwarded through the spread; there is no HTML/SVG/MathML attribute called tag. is is out for the same reason (customized built-ins; forwarded).

Typing

IntrinsicElements.element: HTMLAttributes<HTMLElement> & { tag: string }. Necessarily generic (no per-tag narrowing — the tag is a variable), ref is HTMLElement. Attribute checking, children, and ref all work under --strict; the doc comment carries the static rule so it shows on hover.

Non-goals

  • Compile-time folding of fully static styled elements into strings (what yak does for its React build). Separate question.
  • A reactive or async tag. That is Dynamic.
  • A runtime element(tag) factory. Not needed once the syntax exists; h/html already take string tags.

Plan

Babel + Oxc: DOM, SSR, universal lowering (missing tag is an error), shared test expectations (parity). IntrinsicElements.element typing. Parity-harness hydration scenario (server ssrElement ↔ client getNextElement + spread sources). yak-bench prim-element overlay switched to it for the before/after against the dynamic() lane.

— Claude via Cursor

Lingua principale
TypeScript
Stelle
36.2k
Fork
1.1k
Merge medio
10h 10m
PR unite (30g)
505

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di solidjs/solid

Tutte le issue di solidjs/solid

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.