v0.1.0-dev.727

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

~100x faster SSR than Next.js. Compiles to native code instead of running JavaScript.

Compile-Time Safety

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

Familiar Syntax

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

Server-Side Rendering

Server-side rendering by default. Pages render on the server with zero configuration.

Static Site Generation

Static site generation by default. Pages render on the server with zero configuration.

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, switch, standard control flow works 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
40k
FrameworkZiex
Requests/sec40,211
P50 Latency0.60 ms
P99 Latency27.54 ms
25k
FrameworkLeptos
Requests/sec25,169
P50 Latency1.76 ms
P99 Latency5.31 ms
21k
FrameworkDioxus
Requests/sec21,423
P50 Latency1.43 ms
P99 Latency35.50 ms
5.0k
FrameworkSolidStart
Requests/sec4,994
P50 Latency8.80 ms
P99 Latency26.97 ms
743
FrameworkNext.js
Requests/sec743
P50 Latency65.02 ms
P99 Latency110.55 ms
Dockerized SSR endpoint hit with 10k requests at 50 concurrent connections, averaged over 3 runs.
Peak CPU usage during load (lower is better)
Ziex
55.5%
FrameworkZiex
Peak CPU55.5%
Requests/sec40,211
82.3%
FrameworkLeptos
Peak CPU82.3%
Requests/sec25,169
90.2%
FrameworkDioxus
Peak CPU90.2%
Requests/sec21,423
152.3%
FrameworkSolidStart
Peak CPU152.3%
Requests/sec4,994
175.1%
FrameworkNext.js
Peak CPU175.1%
Requests/sec743
Maximum CPU utilization during the load test. Lower means more efficient resource usage.
Peak memory under load (lower is better)
3.7 MB
FrameworkLeptos
Idle Memory9.1 MB
Peak Memory3.7 MB
Ziex
5.3 MB
FrameworkZiex
Idle Memory17.0 MB
Peak Memory5.3 MB
6.3 MB
FrameworkDioxus
Idle Memory28.2 MB
Peak Memory6.3 MB
103.0 MB
FrameworkSolidStart
Idle Memory131.9 MB
Peak Memory103.0 MB
213.2 MB
FrameworkNext.js
Idle Memory96.3 MB
Peak Memory213.2 MB
Peak container RSS during load test. Lower is better. Hover for idle memory.
Cold start time in milliseconds (lower is better)
213 ms
FrameworkDioxus
Cold Start213 ms
Image Size115 MB
233 ms
FrameworkLeptos
Cold Start233 ms
Image Size24 MB
Ziex
240 ms
FrameworkZiex
Cold Start240 ms
Image Size18 MB
317 ms
FrameworkSolidStart
Cold Start317 ms
Image Size364 MB
514 ms
FrameworkNext.js
Cold Start514 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
24 MB
FrameworkLeptos
Image Size24 MB
115 MB
FrameworkDioxus
Image Size115 MB
297 MB
FrameworkNext.js
Image Size297 MB
364 MB
FrameworkSolidStart
Image Size364 MB
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
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!");
}

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");