Silk is an embedded DSL for authoring HTML from TypeScript. You write simple
typed JSX and Silk generates ReadableStream
s of
HTMLToken
s.
Child nodes and attributes can be async values or streams.
Here's an example:
import { createElement } from '@matt.kantor/silk'
const document = (
<html lang="en">
<head>
<title>Greeting</title>
</head>
<body>Hello, {slowlyGetPlanet()}!</body>
</html>
)
const slowlyGetPlanet = (): Promise<ReadableHTMLTokenStream> =>
new Promise(resolve =>
setTimeout(() => resolve(<strong>world</strong>), 2000),
)
The HTML structure and content before the slowlyGetPlanet
call will
immediately be readable from the document
stream, while the rest will appear
as soon as the Promise
returned by slowlyGetPlanet
resolves.
To use Silk, add these options to your tsconfig.json
1:
"jsx": "react",
"jsxFactory": "createElement",
"jsxFragmentFactory": "createElement",
Also, import { createElement } from '@matt.kantor/silk'
in each of your .tsx
files.
If you're using Silk for server-side rendering and want a stream to pipe out as
the HTTP response, HTMLSerializingTransformStream
has you covered. Here's an
example of an HTTP server which uses Silk to serve a web page:
import { createServer } from 'node:http'
import { Writable } from 'node:stream'
import {
type ReadableHTMLTokenStream,
createElement,
HTMLSerializingTransformStream,
} from '@matt.kantor/silk'
const port = 80
createServer((_request, response) => {
const document = (
<html lang="en">
<head>
<title>Greeting</title>
</head>
<body>Hello, {slowlyGetPlanet()}!</body>
</html>
)
response.setHeader('Content-Type', 'text/html; charset=utf-8')
document
.pipeThrough(
new HTMLSerializingTransformStream({
includeDoctype: true,
}),
)
.pipeTo(Writable.toWeb(response))
.catch(console.error)
}).listen(port)
const slowlyGetPlanet = (): Promise<ReadableHTMLTokenStream> =>
new Promise(resolve =>
setTimeout(() => resolve(<strong>world</strong>), 2000),
)
If you run that and make a request to it from a web browser, you'll see "Hello, " appear quickly, then "world!" appear after two seconds. You can try it on StackBlitz.
Silk can also be used client-side by translating the stream of
HTMLToken
s into DOM method calls. You can see a complete
example of this on StackBlitz.
HTML is inherently streamable, yet many web servers buffer the entire response body before sending a single byte of it to the client. This leaves performance on the table—web browsers are perfectly capable of incrementally parsing and rendering partial HTML documents as they arrive.
Streaming is especially valuable when the document references external resources (e.g. stylesheets). By sending HTML to the client while the server continues asynchronous work, the browser can fetch those resources concurrently with that work, significantly reducing the time required to display the page.
Streaming also helps keep server memory utilization low as data already sent to the client can be freed.
There are no non-HTML attributes (like ref
and key
), attribute names are
always spelled exactly as they are in HTML (e.g. class
is not className
),
and attribute values are plain strings.
Silk doesn't have "components" as part of its JSX syntax—all elements are
intrinsic. If you want to abstract/reuse bits of HTML, you can call functions
from within {…}
blocks:
<div>{profile(userID)}</div>
JSX elements are strictly-typed, with each element only accepting its known attributes, attributes only accepting known values, and void elements forbidding children. Tag names and attributes are suggested in completions, and documentation from MDN is presented in type info:
Footnotes
-
"jsx": "react"
may seem odd because Silk isn't related to React, but TypeScript's JSX configuration is based around React's semantics. ↩