-
-
Notifications
You must be signed in to change notification settings - Fork 138
feat: Vite SSG Optimized (Thread Workers) v0.28.0 #458
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
base: main
Are you sure you want to change the base?
feat: Vite SSG Optimized (Thread Workers) v0.28.0 #458
Conversation
… --skip-build param to reuse last server and client builds(usefull for debugging)
…o work with string, is 7.2 times faster than JSDOM
…ring substitute version but stil faster then JSDOM), some performance optimizations includes only inject html code once instead of using replace and JSDOM
…adding more tests
… HydrationMode when data-server-rendered is found on browser
…e whitespaces due breaks hydration
…o separated process. Saves memory
…ions into separated process. Saves memory" This reverts commit b8a1fcb.
…TTY inside worker
…ll references from vite-ssg to vite-ssg-optimized
uhmmmmm, did you tried using // main.ts
export const createApp = ViteSSG(
App,
{
routes: setupLayouts(routes),
base: import.meta.env.BASE_URL,
scrollBehavior(to) {
if (to.hash) {
return {
el: decodeURIComponent(to.hash),
// top: 120,
behavior: 'smooth',
}
}
else {
return new Promise((resolve) => {
setTimeout(() => resolve({ left: 0, top: 0 }), 300)
})
}
},
},
(ctx) => {
const pinia = createPinia()
ctx.app.use(pinia)
const modules = import.meta.glob<{
install: UserModule
}>('./modules/*.ts', {
eager: true,
})
for (const { install } of Object.values(modules)) {
install?.(ctx)
}
if (!import.meta.env.SSR) {
ctx.router.beforeEach(async (to, from, next) => {
const store = useAuthStore(pinia)
if (!store.ready)
store.initialize()
next()
})
}
},
{
hydration: !import.meta.env.DEV, // <=== HERE THE HACK
// transformState: state => state,
transformState(state) {
if (import.meta.env.DEV || import.meta.env.SSR) {
return JSON.stringify({})
}
return state
},
},
) |
|
||
type PreloadLinkTransport = Document | { html:string } | ||
|
||
export function buildPreloadLinks< |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be optional, I'm using this but using HTTP Link header (instead links in the html) for HTTP/2 (or HTTP/3, QUIC) via preload.json generation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you show an example how you did this prealod.json with HTTP/3, QUIC?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I cannot since our backend cannot use http/3 (quic) yet, but it is the same concept using http/2 , you just add the http header link for the corresponding page. Http/3 and quic will require to send an extra early hints header. The main change from original http/2 behavior is about that this http header link is just a hint for the browser. The preload http header can be used by the browser even before start parsing the htnl response
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here you can see how we prepare netlify _headers to the add corresponding http header link per page at VitePress docs vuejs/vitepress#4814
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I read vuejs/vitepress#4814 and understood what this example does.
But the original vite-ssg does the same thing as my pull request.
the export function buildPreloadLinks
is a replacement for the JSDOM renderPreloadLinks.
See the original implementation at
https://github.com/antfu-collective/vite-ssg/blob/main/src/node/build.ts:178
// create jsdom from renderedHTML
const jsdom = new JSDOM(renderedHTML)
// render current page's preloadLinks
renderPreloadLinks(jsdom.window.document, ctx.modules || new Set<string>(), ssrManifest)
If the current version are correct the draft version might be too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is not the same, previous code is writting the headers in the html, I'm writing those links in the HTTP Link Response header; with previous code, the browser requires to start parsing the html, with the HTTP Link Header, the browser knows what modules will require the incoming page even before receiving the first html byte
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
with the HTTP Link Header, the browser knows what modules will require the incoming page even before receiving the first html byte
Both original vite-ssg:main and my pull requests writes the Links to html, the ssrHead.headTags
will be injected to the <html><head>
The injection of preload links on the tag html > head
is made by JSDOM
in the original code.
Both original and pull request code have the const html
with the preloads tags <link>
in their contents.
pull-request
const preloads:string[] = buildPreloadLinks({ html: transformedIndexHTML }, ctx.modules || new Set<string>(), ssrManifest)
let ssrHead = {
headTags: preloads.join("\n"),
bodyAttrs: '',
htmlAttrs: '',
bodyTagsOpen: '',
bodyTags: '',
}
//inject the ssrHead preloadLinks to the html
const html = await renderHTML({
rootContainerId,
indexHTML: transformedIndexHTML,
appHTML,
initialState: transformState(initialState),
ssrHead,
teleports: ctx.teleports,
})
Original
// create jsdom from renderedHTML
const jsdom = new JSDOM(renderedHTML)
// render current page's preloadLinks
renderPreloadLinks(jsdom.window.document, ctx.modules || new Set<string>(), ssrManifest)
// render head
if (head)
await renderDOMHead(head, { document: jsdom.window.document })
const html = jsdom.serialize()
R = T extends Document ? HTMLLinkElement : string | ||
>(document: T, attrs: Record<string, any>): R|undefined { | ||
if(!('querySelector' in document)){ | ||
const regex = new RegExp(`<link[^>]*href\s*=\s*("|')${attrs.href}\\1[^>]*>`,'m') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you also need to parse script tags with type=module, the entry point and handle crossorigin attrs, this is what I'm using to generate .vite/preload.json
file via ultrahtml library on the finish hook:
.vite/preload.json generation
const data: {
[id: string]: {
path: string
as: 'font' | 'script' | 'style'
rel: 'preload' | 'modulepreload'
crossorigin: boolean
}[]
} = {}
for (const file of files) {
let id = file.replace(/\.html$/, '')
if (id === 'index') {
id = '/'
}
// data[id] = []
data[id] = [{
path: '/fonts/roboto-v20-latin-regular.woff2',
as: 'font',
rel: 'preload',
crossorigin: true,
}]
/* data[id] = [{
path: '/fonts/roboto-v20-latin-regular.woff2',
as: 'font',
rel: 'preload',
crossorigin: true,
}, {
path: '/fonts/roboto-v20-latin-500.woff2',
as: 'font',
rel: 'preload',
crossorigin: true,
}] */
const preload = data[id]
const preloads = parse(await fsp.readFile(path.resolve(cwd, file), 'utf8'))
await walk(preloads, (node) => {
// extract all scripts type="module"
if (node.type === ELEMENT_NODE && node.name === 'script') {
const type = node.attributes.type
if (type === 'module') {
const src = node.attributes.src
if (src) {
preload.push({
path: src,
as: 'script',
rel: 'modulepreload',
crossorigin: node.attributes.crossorigin === 'true' || node.attributes.crossorigin === '',
})
}
}
}
// extract all links rel="preload" as="styles"
if (node.type === ELEMENT_NODE && node.name === 'link') {
const rel = node.attributes.rel
const as = node.attributes.as
if (rel === 'preload') {
if (as === 'style') {
const href = node.attributes.href
if (href) {
preload.push({
path: href,
as: 'style',
rel: 'preload',
crossorigin: node.attributes.crossorigin === 'true' || node.attributes.crossorigin === '',
})
}
}
}
else if (rel === 'modulepreload') {
const href = node.attributes.href
if (href) {
preload.push({
path: href,
as: 'script',
rel: 'modulepreload',
crossorigin: node.attributes.crossorigin === 'true' || node.attributes.crossorigin === '',
})
}
}
}
})
}
const preloadData = Object.entries(data).reduce((acc, [url, hints]) => {
const links: string[] = []
for (const link of hints) {
links.push(`<${link.path[0] === '/' ? link.path : `/${link.path}`}>; rel=${link.rel}; as=${link.as}${link.crossorigin ? `; crossorigin` : ''}`)
}
acc[url] = links.join(', ')
return acc
}, {} as Record<string, string>)
await fsp.writeFile(path.join(cwd, '.vite/preload.json'), JSON.stringify(preloadData, null, 2), 'utf8')
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
preload.json
I can't found any 'preload.json' in the repository antfu-collective/vite-ssg
.
Where the 'preload.json' is consumed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This a custom plugin for a project at work where we use a custom backend. The preload json file is generated via vite-ssg build end hook
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
buildPreloadLinks does this.
The option |
@userquin When I have more time I will check. I did't have too much experiences with pull requests workflow in open source software. Thank you. |
IIRC hydration option there also in old versions, I don't remember when it was added. |
It's present on version v25.2.0, not before that. |
Thanks for this patch. It’s very useful in non-async-safe cases where you need to change a global during (like |
Description
The original implementation have problems with performance when it rendering many routes.
Replaced @unhead/dom by @unhead/ssr
Used a custom injectInHtml using html5Parser instead of jsdom with is faster
Added worker threads to avoid locking main event loop
Avoid grow the number of tasks in queue when we have many routes
Added teleport support, the original vite-ssg don't writes teleports to the final file causing SSR mismatches
Detect if is in isHydrationMode by quering [data-server-rendered] on client
Creates SSRApp or App accord with environment and hydration
Same changes of PR #457 but updated to current version 0.28.0 and resolved conflicts.