Skip to content

Commit

Permalink
Add support for HTTP/2 for better dev performance. (tajo#379)
Browse files Browse the repository at this point in the history
* Replace express with koa

* Add koa types

* Add support for HTTP/2 for better dev performance.
  • Loading branch information
tajo authored Feb 17, 2023
1 parent 6da7689 commit 6bce3dd
Show file tree
Hide file tree
Showing 6 changed files with 447 additions and 58 deletions.
5 changes: 5 additions & 0 deletions .changeset/tall-rice-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ladle/react": minor
---

Add support for HTTP/2 for better dev performance.
142 changes: 97 additions & 45 deletions packages/ladle/lib/cli/vite-dev.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { createServer, searchForWorkspaceRoot } from "vite";
import express from "express";
import koa from "koa";
import http from "http";
import http2 from "http2";
import c2k from "koa-connect";
import path from "path";
import getPort from "get-port";
import { globby } from "globby";
import boxen from "boxen";
Expand All @@ -15,7 +19,7 @@ import { getEntryData } from "./vite-plugin/parse/get-entry-data.js";
* @param configFolder {string}
*/
const bundler = async (config, configFolder) => {
const app = express();
const app = new koa();
const port = await getPort({
port: [config.port, 61001, 62002, 62003, 62004, 62005],
});
Expand All @@ -42,55 +46,103 @@ const bundler = async (config, configFolder) => {
});
const vite = await createServer(viteConfig);
const { moduleGraph, ws } = vite;
app.head("*", async (_, res) => res.sendStatus(200));
app.get("/meta.json", async (_, res) => {
const entryData = await getEntryData(
await globby(
Array.isArray(config.stories) ? config.stories : [config.stories],
),
);
const jsonContent = getMetaJsonObject(entryData);
res.json(jsonContent);
});
// When `middlewareMode` is true, vite's own base middleware won't redirect requests,
// so we need to do that ourselves.
const { base } = viteConfig;
if (base && base !== "/" && base !== "./") {
app.get("/", (_, res) => res.redirect(base));
app.get("/index.html", (_, res) => res.redirect(base));
}
app.use(vite.middlewares);
const serverUrl = `${vite.config.server.https ? "https" : "http"}://${
vite.config.server.host || "localhost"
}:${port}${vite.config.base || ""}`;
app.listen(
port,
const redirectBase = base && base !== "/" && base !== "./" ? base : "";

app.use(async (ctx, next) => {
if (
ctx.request.method === "GET" &&
ctx.request.url ===
(redirectBase ? path.join(redirectBase, "meta.json") : "/meta.json")
) {
const entryData = await getEntryData(
await globby(
Array.isArray(config.stories) ? config.stories : [config.stories],
),
);
const jsonContent = getMetaJsonObject(entryData);
ctx.body = jsonContent;
return;
}
if (redirectBase && ctx.request.method === "GET") {
if (ctx.request.url === "/" || ctx.request.url === "/index.html") {
ctx.redirect(redirectBase);
return;
}
if (ctx.request.url === "/meta.json") {
ctx.redirect(path.join(redirectBase, "meta.json"));
return;
}
}
if (ctx.request.method === "HEAD") {
ctx.status = 200;
return;
}
await next();
});
app.use(c2k(vite.middlewares));

// activate https if key and cert are provided
const useHttps =
typeof vite.config.server?.https === "object" &&
vite.config.server.https.key &&
vite.config.server.https.cert;
const hostname =
vite.config.server.host === true
? "0.0.0.0"
: typeof vite.config.server.host === "string"
? vite.config.server.host
: "localhost",
async () => {
console.log(
boxen(`🥄 Ladle.dev served at ${serverUrl}`, {
padding: 1,
margin: 1,
borderStyle: "round",
borderColor: "yellow",
titleAlignment: "center",
textAlignment: "center",
}),
);
: "localhost";
const serverUrl = `${useHttps ? "https" : "http"}://${hostname}:${port}${
vite.config.base || ""
}`;

if (
vite.config.server.open !== "none" &&
vite.config.server.open !== false
) {
const browser = /** @type {string} */ (vite.config.server.open);
await openBrowser(serverUrl, browser);
}
},
);
const listenCallback = async () => {
console.log(
boxen(`🥄 Ladle.dev served at ${serverUrl}`, {
padding: 1,
margin: 1,
borderStyle: "round",
borderColor: "yellow",
titleAlignment: "center",
textAlignment: "center",
}),
);

if (
vite.config.server.open !== "none" &&
vite.config.server.open !== false
) {
const browser = /** @type {string} */ (vite.config.server.open);
await openBrowser(serverUrl, browser);
}
};

if (useHttps) {
http2
.createSecureServer(
{
// Support HMR WS connection
allowHTTP1: true,
maxSessionMemory: 100,
settings: {
// Note: Chromium-based browser will initially allow 100 concurrent streams to be open
// over a single HTTP/2 connection, unless HTTP/2 server advertises a different value,
// in which case it will be capped at maximum of 256 concurrent streams. Hence pushing
// to the limit while in development, in an attempt to maximize the dev performance by
// minimizing the chances of the module requests queuing/stalling on the client-side.
// @see https://source.chromium.org/chromium/chromium/src/+/4c44ff10bcbdb2d113dcc43c72f3f47a84a8dd45:net/spdy/spdy_session.cc;l=477-479
maxConcurrentStreams: 256,
},
// @ts-ignore
...vite.config.server.https,
},
app.callback(),
)
.listen(port, hostname, listenCallback);
} else {
http.createServer(app.callback()).listen(port, hostname, listenCallback);
}

// trigger full reload when new stories are added or removed
const watcher = chokidar.watch(config.stories, {
Expand Down
4 changes: 3 additions & 1 deletion packages/ladle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@
"cross-spawn": "^7.0.3",
"debug": "^4.3.4",
"default-browser": "^3.1.0",
"express": "^4.18.2",
"get-port": "^6.1.2",
"globby": "^13.1.3",
"history": "^5.3.0",
"koa": "^2.14.1",
"koa-connect": "^2.1.0",
"lodash.merge": "^4.6.2",
"open": "^8.4.0",
"prism-react-renderer": "^1.3.5",
Expand Down Expand Up @@ -84,6 +85,7 @@
"@types/cross-spawn": "^6.0.2",
"@types/debug": "^4.1.7",
"@types/express": "^4.17.17",
"@types/koa": "^2.13.5",
"@types/lodash.merge": "^4.6.7",
"@types/node": "^18.11.19",
"@types/ws": "^8.5.4",
Expand Down
58 changes: 58 additions & 0 deletions packages/website/docs/http2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
id: http2
title: HTTP/2
---

**You can significantly boost Ladle's dev server performance by activating HTTP/2**. How does it work? Ladle is powered by Vite and Vite serves each module individually over HTTP since it's not creating bundles.

Some of the bigger stories might import hundreds or even thousands of modules. That's a lot of individual network requests. It can dramatically slow down the browser.

HTTP/2 solves this problem by allowing multiple requests to be sent over the same connection. This means that the browser can send multiple requests at the same time, and the server can send multiple responses at the same time.

## Configuration

HTTP/2 requires an additional configuration - a valid SSL certificate. Fortunately, **it's very easy to do with [mkcert](https://github.com/FiloSottile/mkcert)**!

### macOS

[Linux](https://github.com/FiloSottile/mkcert#linux). [Windows](https://github.com/FiloSottile/mkcert#windows).

```bash
brew install mkcert #assuming you have homebrew installed
mkcert -install #creates a new local certificate authority
mkcert localhost #creates a new certificate for localhost
```

Expect to be prompted for your OS password. This will create two files: `localhost.pem` and `localhost-key.pem`. Now we need to pass them into the `vite.config.ts`:

```ts
import { defineConfig } from "vite";
import fs from "fs";

export default defineConfig({
server: {
https: {
key: fs.readFileSync("./localhost-key.pem"),
cert: fs.readFileSync("./localhost.pem"),
},
},
});
```

That's it. Now you can run `pnpm ladle serve` and Ladle will be served over HTTP/2! The default URL `https://localhost:61000` should be automatically opened in your browser.

## Alternatives

Alternatively, you can use node based tool called [devcert](https://github.com/davewasmer/devcert).

There is also [vite-plugin-basic-ssl](https://github.com/vitejs/vite-plugin-basic-ssl) that's really easy to setup; however, it generates only untrusted certificates so you'll be getting warnings in your browser.

## Security

**Be sure to never share the root key that these tools create!**

Devcert docs provides a good reason why:

> There's a reason that your OS prompts you for your root password when devcert attempts to install it's root certificate authority. By adding it to your machine's trust stores, your browsers will automatically trust any certificate generated with it.
> This exposes a potential attack vector on your local machine: if someone else could use the devcert certificate authority to generate certificates, and if they could intercept / manipulate your network traffic, they could theoretically impersonate some websites, and your browser would not show any warnings (because it trusts the devcert authority).
2 changes: 1 addition & 1 deletion packages/website/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ module.exports = {
{
type: "category",
label: "Configuration",
items: ["cli", "config", "programmatic", "swc"],
items: ["cli", "config", "programmatic", "swc", "http2"],
},
{
type: "category",
Expand Down
Loading

0 comments on commit 6bce3dd

Please sign in to comment.