Skip to content

Commit

Permalink
vite dev partial support
Browse files Browse the repository at this point in the history
  • Loading branch information
wille committed Aug 21, 2024
1 parent 731d09c commit b50363c
Show file tree
Hide file tree
Showing 6 changed files with 318 additions and 205 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ jobs:
run: npm test
- name: Format
run: npx prettier --check ./src
- name: Build
run: npm run build
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
[![NPM package](https://img.shields.io/npm/v/vite-preload.svg?style=flat-square)](https://www.npmjs.com/package/vite-preload)

# vite-preload

This plugin will significantly speed up your server rendered vite application by preloading async modules as early as possible and it will avoid Flash Of Unstyled Content (FOUC) by including stylesheets from async modules in the initial HTML.

## Explainer

Vite supports `React.lazy()` just fine but any lazy imported modules and its CSS imports will not be injected into html or DOM until the entrypoint module has imported them.
Vite supports `React.lazy()` and dynamic imports just fine but any lazy imported modules and its CSS imports will not be injected into html or DOM until the entrypoint module has imported them.

It's a common pattern to have each page/route in your application lazy loaded especially if you are migrating to Vite from something else like webpack with loadable-components.

Expand Down Expand Up @@ -111,11 +113,12 @@ function render(req, res) {
```

> [!WARN]
> There is CURRENTLY no support for preloading JS in the development environment, while not as important as CSS, not preloading CSS
> will still give you a unpleasant experience with Flash Of Unstyled Content in the development environment
> There is CURRENTLY no support for CSS in the development environment because Vite handles CSS as inline style tags so you will still experience some Flash Of Unstyled Content .
## Further optimizations

### Preloading `React.lazy`

If your app knows what pages or components that should be preloaded, like the next obvious path the user will make in your user flow, it's recommended to lazy load them with something like `lazyWithPreload`.

Even if you would use a `import()` call to preload the chunk, React will still suspend
Expand All @@ -124,4 +127,8 @@ import lazyWithPreload from 'react-lazy-with-preload';

const Card = lazyWithPreload(() => import('./Card'));
Card.preload();
```
```

### react-router Lazy Routes

See https://reactrouter.com/en/main/guides/ssr#lazy-routes
10 changes: 5 additions & 5 deletions playground/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@
"@vitejs/plugin-react": "^4.2.1",
"cross-env": "^7.0.3",
"typescript": "^5.4.5",
"vite": "^5.2.10"
"vite": "^5.4.2"
}
}
287 changes: 151 additions & 136 deletions playground/server.js
Original file line number Diff line number Diff line change
@@ -1,163 +1,178 @@
import fs from 'node:fs/promises'
import express from 'express'
import { Transform, Writable } from 'node:stream'
import { ChunkCollector, createHtmlTag, createLinkHeader } from '../dist/index.js'
import fs from 'node:fs/promises';
import express from 'express';
import { Transform, Writable } from 'node:stream';
import {
ChunkCollector,
createHtmlTag,
createLinkHeader,
} from '../dist/index.js';

// Constants
const isProduction = process.env.NODE_ENV === 'production'
const port = process.env.PORT || 5173
const base = process.env.BASE || '/'
const isProduction = process.env.NODE_ENV === 'production';
const port = process.env.PORT || 5173;
const base = process.env.BASE || '/';

// Cached production assets
const templateHtml = isProduction
? await fs.readFile('./dist/client/index.html', 'utf-8')
: ''
? await fs.readFile('./dist/client/index.html', 'utf-8')
: '';
const manifest = isProduction
? JSON.parse(await fs.readFile('./dist/client/.vite/manifest.json', 'utf-8'))
: undefined
? JSON.parse(
await fs.readFile('./dist/client/.vite/manifest.json', 'utf-8')
)
: undefined;

// Create http server
const app = express()
const app = express();

// Add Vite or respective production middlewares
let vite
let vite;
if (!isProduction) {
const { createServer } = await import('vite')
vite = await createServer({
server: { middlewareMode: true },
appType: 'custom',
base,
})
app.use(vite.middlewares)
const { createServer } = await import('vite');
vite = await createServer({
server: { middlewareMode: true },
appType: 'custom',
base,
});
app.use(vite.middlewares);
} else {
const compression = (await import('compression')).default
const sirv = (await import('sirv')).default
app.use(compression())
app.use(base, sirv('./dist/client', { extensions: [] }))
const compression = (await import('compression')).default;
const sirv = (await import('sirv')).default;
app.use(compression());
app.use(base, sirv('./dist/client', { extensions: [] }));
}

// Serve HTML
app.use('*', async (req, res) => {
try {
const url = req.originalUrl.replace(base, '')

let template
let render
if (!isProduction) {
// Always read fresh template in development
template = await fs.readFile('./index.html', 'utf-8')
template = await vite.transformIndexHtml(url, template)
render = (await vite.ssrLoadModule('/src/entry-server.tsx')).render
} else {
template = templateHtml
render = (await import('./dist/server/entry-server.js')).render
try {
const url = req.originalUrl.replace(base, '');

let template;
let render;
if (!isProduction) {
// Always read fresh template in development
template = await fs.readFile('./index.html', 'utf-8');
template = await vite.transformIndexHtml(url, template);
render = (await vite.ssrLoadModule('/src/entry-server.tsx')).render;
} else {
template = templateHtml;
render = (await import('./dist/server/entry-server.js')).render;
}

let didError = false;

// const { pipe, abort } = render(url, ssrManifest, {
// onShellError() {
// res.status(500)
// res.set({ 'Content-Type': 'text/html' })
// res.send('<h1>Something went wrong</h1>')
// },
// onShellReady() {
// res.status(didError ? 500 : 200)
// res.set({ 'Content-Type': 'text/html' })

// const transformStream = new Transform({
// transform(chunk, encoding, callback) {
// res.write(chunk, encoding)
// callback()
// }
// })

// const [htmlStart, htmlEnd] = template.split(`<!--app-html-->`)

// res.write(htmlStart)

// transformStream.on('finish', () => {
// res.end(htmlEnd)
// })

// pipe(transformStream)
// },
// onError(error) {
// didError = true
// console.error(error)
// }
// })
const collector = new ChunkCollector({
manifest,
viteDevServer: vite,
entrypoint: isProduction ? 'index.html' : '/src/entry-client.tsx',
});

const html = await renderStream(render, collector);

const modules = collector.getSortedModules();

const DISABLE_PRELOADING = false;

if (!DISABLE_PRELOADING) {
console.log('Modules used', modules);
res.append('link', collector.getLinkHeader());
}

const tags = DISABLE_PRELOADING ? '' : collector.getTags(true);

res.write(
template
.replace('</head>', `${tags}\n</head>`)
.replace('<!--app-html-->', html)
);
res.end();
} catch (e) {
vite?.ssrFixStacktrace(e);
console.log(e.stack);
res.status(500).end(e.stack);
}

let didError = false

// const { pipe, abort } = render(url, ssrManifest, {
// onShellError() {
// res.status(500)
// res.set({ 'Content-Type': 'text/html' })
// res.send('<h1>Something went wrong</h1>')
// },
// onShellReady() {
// res.status(didError ? 500 : 200)
// res.set({ 'Content-Type': 'text/html' })

// const transformStream = new Transform({
// transform(chunk, encoding, callback) {
// res.write(chunk, encoding)
// callback()
// }
// })

// const [htmlStart, htmlEnd] = template.split(`<!--app-html-->`)

// res.write(htmlStart)

// transformStream.on('finish', () => {
// res.end(htmlEnd)
// })

// pipe(transformStream)
// },
// onError(error) {
// didError = true
// console.error(error)
// }
// })
const collector = new ChunkCollector({ manifest });

const html = await renderStream(render, collector);

const modules = collector.getModules();

const DISABLE_PRELOADING = false;

if (!DISABLE_PRELOADING) {
console.log('Modules used', modules);
res.append('link', collector.getLinkHeader());
}

const tags = DISABLE_PRELOADING ? '' : collector.getTags(true);

res.write(template
.replace('</head>', `${tags}\n</head>`)
.replace('<!--app-html-->', html)
)
res.end()
} catch (e) {
vite?.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)
}
})
});

// Start http server
app.listen(port, () => {
console.log(`Server started at http://localhost:${port}`)
})
console.log(`Server started at http://localhost:${port}`);
});

async function renderStream(render, collector) {
const start = performance.now();
let html = '';
const stream = new Writable({
write(chunk, encoding, callback) {
html += chunk.toString();
console.log('chunk', chunk.toString().length, performance.now() - start);
callback();
},
});

const renderPromise = new Promise((resolve, reject) => {
stream.on('finish', () => {
resolve();
});
stream.on('error', e => {
reject(e);
const start = performance.now();
let html = '';
const stream = new Writable({
write(chunk, encoding, callback) {
html += chunk.toString();
console.log(
'chunk',
chunk.toString().length,
performance.now() - start
);
callback();
},
});

const { pipe } = render('', collector, {
onShellReady() {
console.log('shellready', performance.now() - start);
},
onShellError(error) {
console.error('error', error);
reject(error);
},
onAllReady() {
console.log('allready', performance.now() - start);
},
onError(error) {
console.error('error', error);
reject(error);
},
const renderPromise = new Promise((resolve, reject) => {
stream.on('finish', () => {
resolve();
});
stream.on('error', (e) => {
reject(e);
});

const { pipe } = render('', collector, {
onShellReady() {
console.log('shellready', performance.now() - start);
},
onShellError(error) {
console.error('error', error);
reject(error);
},
onAllReady() {
console.log('allready', performance.now() - start);
},
onError(error) {
console.error('error', error);
reject(error);
},
});

pipe(stream);
});

pipe(stream);
});

await renderPromise;
return html;
await renderPromise;
return html;
}
Loading

0 comments on commit b50363c

Please sign in to comment.