Minimal example of using V8 to render a Frontend App built with Vite.
- v8go - Go bindings for V8 JavaScript engine
- Vitesse Lite - Frontend App Template written in Vue and built with Vite
- Fully functional frontend application (not just a simple "Hello World")
- SSR for the frontend application
- V8 Isolate pool to avoid creating a new Isolate for each request
- Embedded frontend application in the binary to reduce the file system calls
docker compose up -d
curl http://localhost:8080/hi/test
Note: if there is some issues with building the image, remove --platform=linux/amd64
options from Dockerfile. This was used to avoid issues when running on Apple M1 architecture.
Pros:
- No need to use Node.js
- All in one binary
Cons:
- The more big js-bundle size the slower rendering
- You can't use Vite features like hot module replacement
- To build the server you need a lot of c-libs installed
The default config is configured in vite.config.ts
and the additional configuration for the SSR build is located in vite.config.prod.ts
.
SSR config builds the frontend application with target cjs
as it is required for the V8 engine.
See:
- Client-side entry:
client/src/entry-client.ts
- Server-side entry:
client/src/entry-server.ts
Split frontend build for server for multiple files (currently it's a single file).
A require
function must be implemented in the V8 context to load the files.
It's better to cache the required files in memory.
It's not possible to use hot reloading with V8. For frontend development it's better to use Vite directly and store code it in another repo. For backend development use any watcher to rebuild all (e.g. air).
Some cool features of Vite will be missing (e.g. Glob Import, Dynamic Import, hot module replacement, etc.), but it's possible to build the frontend application with ESBuild - a Go-based bundler. It has a Go API and it's very fast. Actually, it's used by Vite under the hood.
Fastest one. The idea is to run SSR script, get function with sensitive args and run it.
iso
object has Isolate *v8go.Isolate
and RenderScript *v8go.UnboundScript
(it was compiled before).
// renderer.go
// ...
func (r *Renderer) Render(urlPath string) (string, error) {
iso := r.pool.Get()
defer r.pool.Put(iso)
ctx := v8go.NewContext(iso.Isolate)
defer ctx.Close()
iso.RenderScript.Run(ctx)
renderCmd := fmt.Sprintf(`ssrRender("%s")`, urlPath)
val, err := ctx.RunScript(renderCmd, r.ssrScriptName)
if err != nil {
if jsErr, ok := err.(*v8go.JSError); ok {
err = fmt.Errorf("%v", jsErr.StackTrace)
}
return "", nil
}
return val.String(), nil
}
// entry-server.ts
// ...
function ssrRender(url: string) {
return render(url).then((html) => {
return html
})
}
(globalThis as any).ssrRender = ssrRender
Create global object in Go with render
function that recieves rendered html, concat string in Go and return.
iso
object has Isolate *v8go.Isolate
and RenderScript *v8go.UnboundScript
(it was compiled before).
// renderer.go
// ...
func (r *Renderer) Render(urlPath string) (string, error) {
iso := r.pool.Get()
defer r.pool.Put(iso)
outputHTML := ""
ssrObject := v8go.NewObjectTemplate(iso.Isolate)
ssrObject.Set("href", urlPath)
ssrObject.Set("render", v8go.NewFunctionTemplate(iso.Isolate, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) > 0 {
outputHTML = args[0].String()
}
return nil
}))
globalObject := v8go.NewObjectTemplate(iso.Isolate)
globalObject.Set("ssr", ssrObject)
ctx := v8go.NewContext(iso.Isolate, globalObject)
defer ctx.Close()
start := time.Now()
iso.RenderScript.Run(ctx)
//if _, err := ctx.RunScript(r.scriptSource, r.Path); err != nil {
// return "", err
//}
fmt.Println("Script run:", time.Since(start))
return outputHTML, nil
}
// entry-server.ts
// ...
if (typeof ssr !== 'undefined') {
render(ssr.href).then((html) => {
ssr.render(html)
})
}