v0.1.0-dev.765

Ziex

/

Build fast & type-safe full-stack web apps with Zig.

JSX like syntax or just like HTML with having access to Zig's control flow.

$ curl -fsSL https://ziex.dev/install | bash
> powershell -c "irm ziex.dev/install.ps1 | iex"

Features

It's Fast

Significantly faster at SSR than many other frameworks.

Compile-time Safety

Zig's type system catches bugs at compile time. No runtime surprises, no GC.

Familiar Syntax

Familiar JSX-like syntax, or plain HTML-style markup, with full access to Zig's control flow.

Server-side Rendering

Render per request on the server for dynamic data, auth, and personalized pages for best performance and SEO.

Static Site Generation

Pre-render pages at build/export time into static HTML for fast CDN delivery.

File System Routing

Folder structure defines routes. No configs, no magic strings, just files in folders.

Client-side Rendering

Optional client-side rendering for interactive experiences when you need it.

Control Flow in Zig's Syntax

if/else, for/while, and switch all work as expected. It's just Zig.

Developer Tooling

CLI, hot reload, and editor extensions for the best DX.

Performance

View benchmark →
Each framework runs in a Docker container limited to 2 CPU cores and 2 GB RAM.

Requests per second (higher is better)
Ziex
76k
FrameworkZiex
Requests/sec76,444
P50 Latency0.41 ms
P99 Latency4.01 ms
65k
FrameworkJetzig
Requests/sec65,735
P50 Latency0.29 ms
P99 Latency6.32 ms
26k
FrameworkLeptos
Requests/sec26,926
P50 Latency1.69 ms
P99 Latency4.71 ms
20k
FrameworkDioxus
Requests/sec20,607
P50 Latency1.42 ms
P99 Latency42.11 ms
4.3k
FrameworkSolidStart
Requests/sec4,278
P50 Latency10.03 ms
P99 Latency30.95 ms
617
FrameworkNext.js
Requests/sec617
P50 Latency79.19 ms
P99 Latency139.17 ms
Dockerized SSR endpoint hit with 10k requests at 50 concurrent connections, averaged over 3 runs.
Average CPU usage during load (lower is better)
Ziex
19.5%
FrameworkZiex
Avg CPU19.5%
Peak CPU51.0%
Requests/sec76,444
20.9%
FrameworkJetzig
Avg CPU20.9%
Peak CPU55.7%
Requests/sec65,735
37.1%
FrameworkLeptos
Avg CPU37.1%
Peak CPU90.6%
Requests/sec26,926
41.1%
FrameworkDioxus
Avg CPU41.1%
Peak CPU97.8%
Requests/sec20,607
51.7%
FrameworkSolidStart
Avg CPU51.7%
Peak CPU77.9%
Requests/sec4,278
62.3%
FrameworkNext.js
Avg CPU62.3%
Peak CPU83.0%
Requests/sec617
Average CPU utilization during the load test via cgroup cpu.stat. Hover for peak CPU.
Peak memory under load (lower is better)
Ziex
13.9 MB
FrameworkZiex
Idle Memory8.6 MB
Peak Memory13.9 MB
19.6 MB
FrameworkJetzig
Idle Memory17.8 MB
Peak Memory19.6 MB
24.4 MB
FrameworkLeptos
Idle Memory22.4 MB
Peak Memory24.4 MB
38.1 MB
FrameworkDioxus
Idle Memory31.3 MB
Peak Memory38.1 MB
186.3 MB
FrameworkSolidStart
Idle Memory50.3 MB
Peak Memory186.3 MB
376.0 MB
FrameworkNext.js
Idle Memory174.9 MB
Peak Memory376.0 MB
Peak container RSS during load test. Lower is better. Hover for idle memory.
Cold start time in milliseconds (lower is better)
222 ms
FrameworkLeptos
Cold Start222 ms
Image Size24 MB
227 ms
FrameworkDioxus
Cold Start227 ms
Image Size115 MB
241 ms
FrameworkJetzig
Cold Start241 ms
Image Size24 MB
Ziex
246 ms
FrameworkZiex
Cold Start246 ms
Image Size18 MB
341 ms
FrameworkSolidStart
Cold Start341 ms
Image Size364 MB
524 ms
FrameworkNext.js
Cold Start524 ms
Image Size297 MB
Time for the container to start and become ready to serve requests. Lower is better.
Docker image size (lower is better)
Ziex
18 MB
FrameworkZiex
Image Size18 MB
Binary Size3.9 MB
24 MB
FrameworkJetzig
Image Size24 MB
Binary Size10.5 MB
24 MB
FrameworkLeptos
Image Size24 MB
Binary Size10.7 MB
115 MB
FrameworkDioxus
Image Size115 MB
Binary Size9.1 MB
297 MB
FrameworkNext.js
Image Size297 MB
Binary SizeN/A
364 MB
FrameworkSolidStart
Image Size364 MB
Binary SizeN/A
Size of the final Docker image. Smaller images are faster to deploy and use less storage.
Docker image build time in seconds (lower is better)
1m 12s
FrameworkSolidStart
Build Time1m 12s
1m 45s
FrameworkNext.js
Build Time1m 45s
Ziex
2m 53s
FrameworkZiex
Build Time2m 53s
2m 53s
FrameworkJetzig
Build Time2m 53s
10m 18s
FrameworkLeptos
Build Time10m 18s
10m 43s
FrameworkDioxus
Build Time10m 43s
Time to build each framework's Docker image from scratch on the host machine. Lower is better.

Feature Examples

Short, focused snippets that show how each feature feels in practice.

Control Flow
const ControlIfProps = struct { is_admin: bool };
pub fn ControlIf(
    allocator: zx.Allocator,
    props: ControlIfProps,
) zx.Component {
    return (
        <div @{allocator}>
            {if (props.is_admin) (
                <span>Admin</span>
            ) else (
                <span>Member</span>
            )}
        </div>
    );
}
pub fn ControlIfOptional(allocator: zx.Allocator) zx.Component {
    const maybe_name: ?[]const u8 = "Zig";
    return (
        <div @{allocator}>
            {if (maybe_name) |name| (<span>{name}</span>)}
        </div>
    );
}
pub fn ControlIfError(allocator: zx.Allocator) zx.Component {
    const parsed =
        std.fmt.parseInt(u32, "42", 10);

    return (
        <div @{allocator}>
            {if (parsed) |value| (
                <span>{value}</span>
            ) else |err| (
                <span>{@errorName(err)}</span>
            )}
        </div>
    );
}
pub fn ControlWhile(allocator: zx.Allocator) zx.Component {
    var i: usize = 0;
    return (
        <ul @{allocator}>
            {while (i < 3) : (i += 1) (<li>Item {i + 1}</li>)}
        </ul>
    );
}
pub fn ControlWhileOptional(allocator: zx.Allocator) zx.Component {
    var maybe: ?u32 = 1;
    return (
        <ul @{allocator}>
            {while (maybe) |value| : (maybe = if (value < 3) value + 1 else null) (
                <li>{value}</li>
            )}
        </ul>
    );
}
pub fn ControlWhileError(allocator: zx.Allocator) zx.Component {
    var first: bool = true;
    return (
        <div @{allocator}>
            {while (parseOnce(&first)) |value| (
                <span>{value}</span>
            ) else |err| (
                <span>{@errorName(err)}</span>
            )}
        </div>
    );
}

fn parseOnce(first: *bool) !u32 {
    if (first.*) {
        first.* = false;
        return 42;
    }
    return error.Done;
}
Caching
pub fn CacheComponent(allocator: zx.Allocator) zx.Component {
    return (<Expensive @{allocator} @caching="10s" />);
}

fn Expensive(allocator: zx.Allocator) zx.Component {
    // std.Thread.sleep(std.time.ns_per_s * 3);
    return (<div @{allocator}></div>);
}
pub fn Page(
    ctx: zx.PageContext,
) zx.Component {
    // std.Thread.sleep(std.time.ns_per_s * 3);
    return (
        <div @allocator={ctx.arena}>
            <h1>Expensive Page</h1>
            <p>I take too long to load.</p>
        </div>
    );
}

pub const options = zx.PageOptions{
    .caching = .{ .seconds = 300 },
};
File System Routing
pub fn Page(
    ctx: zx.PageContext,
) zx.Component {
    return (
        <div @allocator={ctx.arena}>
            <h1>Home</h1>
        </div>
    );
}
pub fn Layout(
    ctx: zx.LayoutContext,
    children: zx.Component,
) zx.Component {
    return (
        <html @allocator={ctx.arena}>
            <body>
                {children}
            </body>
        </html>
    );
}
Components
const ButtonProps = struct { label: []const u8 };
pub fn Button(
    ctx: *zx.ComponentCtx(ButtonProps),
) zx.Component {
    return (
        <button @allocator={ctx.allocator}>
            {ctx.props.label}
        </button>
    );
}
pub fn Fragment(
    allocator: zx.Allocator,
) zx.Component {
    return (
        <fragment @{allocator}>
            <span>One</span>
            <>
                <p>Two</p><p>Three</p>
            </>
        </fragment>
    );
}
pub fn SpreadProps(
    allocator: zx.Allocator,
) zx.Component {
    const attrs =
        .{ .class = "btn", .id = "cta" };
    const class = "primary";
    return (
        <section @{allocator}>
            <button {..attrs}>Spreading</button>
            <button {class}>Shorthand</button>
        </section>
    );
}
pub fn DynamicAttr(
    allocator: zx.Allocator,
) zx.Component {
    const is_active = true;
    const class = if (is_active) "active" else "idle";
    const color = if (is_active) "green" else "red";
    return (
        <section @{allocator}>
            <button class={class}>Go</button>
            <button style=`color: {color};`>
                Template String
            </button>
        </section>
    );
}
Dynamic Path
pages/[id]/page.zx
pub fn Page(ctx: zx.PageContext) zx.Component {
    const id = ctx.request.getParam("id");
    return (
        <section @allocator={ctx.arena}>
            <h1>User {id}</h1>
        </section>
    );
}
API Route
route.zig
pub fn PUT(ctx: zx.RouteContext) !void {
    try ctx.response.json(.{ .status = "ok" });
}

pub fn POST(ctx: zx.RouteContext) !void {
    const Data = struct { id: u32 };
    const data =
        try ctx.request.json(Data, .{});

    try ctx.response.json(.{
        .id = data.?.id,
        .created = true,
    });
}
WebSocket
routes/ws/route.zig
pub fn GET(ctx: zx.RouteContext) !void {
    try ctx.socket.upgrade({});
}

pub fn Socket(ctx: zx.SocketContext) !void {
    try ctx.socket.write(
        try ctx.fmt("{s}", .{ctx.message}),
    );
}

pub fn SocketOpen(ctx: zx.SocketOpenContext) !void {
    try ctx.socket.write("Opened!");
}
Client-side Rendering
counter.zx
var count: zx.Signal(i32) = .init(0);
pub fn Client(allocator: zx.Allocator) zx.Component {
    return (<Counter @{allocator} @rendering={.client} />);
}
pub fn Counter(allocator: zx.Allocator) zx.Component {
    return (
        <div @{allocator} class="counter">
            <button onclick={increment}>
                Click Me: {&count}
            </button>
        </div>
    );
}
fn increment(_: zx.EventContext) void {
    count.set(count.get() + 1);
}
Preview

Familiar Syntax

Familiar JSX like syntax or just like HTML with having access to Zig's control flow.

example.zx
pub fn QuickExample(allocator: zx.Allocator) zx.Component {
    const is_loading = true;
    const chars = "Hello, ZX Dev!";
    var i: usize = 0;
    return (
        <main @allocator={allocator}>
            <section>
                {if (is_loading) (<h1>Loading...</h1>) else (<h1>Loaded</h1>)}
            </section>

            <section>
                {for (chars) |char| (<span>{char}</span>)}
            </section>

            <section>
                {for (users) |user| (
                    <Profile name={user.name} age={user.age} role={user.role} />
                )}
            </section>

            <section>
                {while (i < 10) : (i += 1) (<p>{i}</p>)}
            </section>
        </main>
    );
}

fn Profile(allocator: zx.Allocator, user: User) zx.Component {
    return (
        <div @allocator={allocator}>
            <h1>{user.name}</h1>
            <p>{user.age}</p>
            {switch (user.role) {
                .admin => (<p>Admin</p>),
                .member => (<p>Member</p>),
            }}
        </div>
    );
}

const UserRole = enum { admin, member };
const User = struct { name: []const u8, age: u32, role: UserRole };

const users = [_]User{
    .{ .name = "John", .age = 20, .role = .admin },
    .{ .name = "Jane", .age = 21, .role = .member },
};

const zx = @import("zx");