| 1 | # cloudflare-preview-benchmark |
| 2 | |
| 3 | The app behind the PR-lifecycle animation on the alchemy.run landing page: an R2 bucket (`Photos`), a KV namespace (`Sessions`), and an Effect-native Worker (`Api`) bound to both. It is deployed per pull request as a preview stage, tested over HTTP, and destroyed. |
| 4 | |
| 5 | | Route | Behavior | |
| 6 | | --- | --- | |
| 7 | | `GET /photos` | JSON gallery listing every object key in `Photos` | |
| 8 | | `PUT /photos/:key` | Uploads the request body to `Photos` | |
| 9 | | `GET /session` | Starts a session and stores it in `Sessions`; with an `x-session-id` header, reads it back | |
| 10 | |
| 11 | This example creates billable Cloudflare infrastructure. Destroy it when you are done. |
| 12 | |
| 13 | ## Run it |
| 14 | |
| 15 | ```sh |
| 16 | bun alchemy deploy --stage pr-147 |
| 17 | STAGE=pr-147 bun test |
| 18 | bun alchemy destroy --stage pr-147 |
| 19 | ``` |
| 20 | |
| 21 | `test/preview.test.ts` deploys through the Alchemy test harness in `beforeAll`. That deploy is a no-op when the stage already exists. Without `STAGE`, the harness uses its default stage, `test_$USER`. The stack is destroyed after the tests only when `CI` is set. |
| 22 | |
| 23 | `Photos` sets `forceDestroy: true` because the upload test leaves an object in the bucket, and R2 refuses to delete a non-empty bucket. |
| 24 | |
| 25 | ## Benchmark |
| 26 | |
| 27 | The "full picture" animation on the landing page (`website/src/components/marketing-islands/PRLifecycle.tsx`) replays these timings in real time for the deploy, test and destroy logs. Re-run the benchmark when Alchemy gets faster or slower, and update the `T_*` constants at the top of that file to match. |
| 28 | |
| 29 | ### Run it |
| 30 | |
| 31 | You need a Cloudflare account and an Alchemy profile with credentials for it (`bunx alchemy login --profile <name>` creates one). |
| 32 | |
| 33 | ```sh |
| 34 | # from the repo root, once |
| 35 | pnpm install |
| 36 | |
| 37 | cd examples/cloudflare-preview-benchmark |
| 38 | ALCHEMY_PROFILE=<name> bun run bench |
| 39 | ``` |
| 40 | |
| 41 | `bun run bench` runs `scripts/bench.sh`, which times one full preview lifecycle and prints one line per phase: |
| 42 | |
| 43 | ``` |
| 44 | deploy 11.62s |
| 45 | url live 0.41s https://api-….workers.dev |
| 46 | test 4.53s |
| 47 | destroy 3.80s |
| 48 | total 20.36s (logs: .alchemy/bench/<timestamp>-*.log) |
| 49 | ``` |
| 50 | |
| 51 | - `STAGE` picks the stage name (default `pr-147`). Use a unique one if several people share an account. |
| 52 | - Each phase's full CLI output is written to `.alchemy/bench/`. The per-resource breakdown below comes from the deploy and destroy logs. |
| 53 | - The script always destroys the stage at the end. If it fails midway, clean up with `bunx alchemy destroy --stage <stage>`. |
| 54 | - Run it three times and take the median; the first run after an install is slower while Bun warms its cache. |
| 55 | |
| 56 | ### Results |
| 57 | |
| 58 | Median of three full cycles, wall clock: |
| 59 | |
| 60 | | Phase | Time | |
| 61 | | --- | --- | |
| 62 | | **Deploy (create 3 resources)** | **11.6s** | |
| 63 | | · CLI startup, stack import, state load | 1.2s | |
| 64 | | · plan | 1.3s | |
| 65 | | · `Sessions` created | 0.7s | |
| 66 | | · `Photos` created | 3.6s | |
| 67 | | · `Api` created (waits for `Photos`, bundles 2.2s, uploads and enables workers.dev 3.3s) | 9.4s | |
| 68 | | **Worker URL returns 200 after deploy** | **+0.4s** | |
| 69 | | **Tests, plain `fetch` against the URL** | **0.7s** | |
| 70 | | **Tests, through the Alchemy harness (`bun test`)** | **4.5s** | |
| 71 | | · GET /photos renders the gallery | 0.24s | |
| 72 | | · uploads a photo to R2 | 0.28s | |
| 73 | | · session survives a reload | 0.26s | |
| 74 | | **Deploy with no changes** | **4.0s** | |
| 75 | | **Deploy after a one-line Worker change** | **7.6s** | |
| 76 | | · new code served at the URL | +0.2s | |
| 77 | | **Destroy** | **3.8s** | |
| 78 | | · `Api` deleted | 0.7s | |
| 79 | | · `Photos` emptied and deleted | 1.4s | |
| 80 | | · `Sessions` deleted | 0.4s | |
| 81 | |
| 82 | Most of the harness test time is the no-op deploy in `beforeAll`: it re-reads every resource from Cloudflare (about 3.2s). The same three tests take 0.7s without it. Running the CLI under Node instead of Bun added about 0.7s per command. |
| 83 | |
| 84 | These numbers come from one Linux machine and one Cloudflare account, using alchemy 2.0.0-beta.79, effect 4.0.0-rc.117 and Bun 1.3.13. Treat them as indicative. |
| 85 | |
| 86 | The landing-page animation also shows a `GitHub.Comment` posting the preview URL. It is not part of this example or the benchmark because it needs a GitHub token and a target PR. |