DEV Community

Cover image for My OSS Projects: Netpack
Florian Rappl
Florian Rappl

Posted on AI-assisted

My OSS Projects: Netpack

A native C# bundler eliminating runtime costs

This is the fifth post in my "My Open-Source Projects" series, where I go through some of the OSS projects I started or maintain and tell you the story behind them. So far: AngleSharp, MAGES, Piral, and Electron.NET. This time it's the youngest of the bunch: Netpack - a project that started as an experiment, is not at 1.0 yet, and has a development story that looks very different from everything above.

What Is Netpack, Actually?

Netpack is a web bundler written in C#/.NET. You point it at an index.html (like you would with Vite or Parcel) or directly at a .js / .ts file, and it follows everything that is referenced from there, bundles it, optimizes it, and writes the result to disk.

npm i -D netpack
npx netpack bundle src/index.html --minify
Enter fullscreen mode Exit fullscreen mode

Yes, I know. Another bundler. I can hear the sigh from here.

oh no, not another JavaScript build tool

But there's a twist that makes this one worth a post: the CLI is a single native binary (no .NET runtime to install, nothing to warm up), it ships with batteries included (TypeScript, JSX, CSS & CSS Modules, Sass / LESS / PostCSS including Tailwind, images, Vue, Astro, Svelte, Solid, ...), and - since it is written in C# - it can also be used as a plain .NET library or an MSBuild task. That last part is where I see the real future of the project. More on that later.

The Question

It all started during my winter vacation with a simple question:

Can web tooling written in C#/.NET compete with tooling written in Rust, Go, or Zig?

Look at the tools we use every day: esbuild is written in Go, rspack and SWC are written in Rust, Bun is written in Zig. These languages are a great fit for native tooling - and the results speak for themselves. Their developer ergonomics, on the other hand, are... let's say "acquired tastes". I'm a .NET guy. I find C# much more approachable than Rust and more capable than Go. So the question was: could .NET work here? Maybe only under certain constraints - but could it?

I wrote it up back in January 2025, included a lot of caveats, and ended with a question to the community: viable idea or just trash? This post is, in a way, the long answer.

Step 1: Fake a Bundler with a Regular Expression

To find out, I started with the most simple approach imaginable - a fake bundler:

  • Open files
  • Read their content
  • Use a regular expression to find things like import statements
  • Resolve the linked modules
  • Look at package.json files to figure out where a package's entry point lives

(If you've read the AngleSharp post you might feel a disturbance right now. Yes: I am aware that "parse code with a regex" is exactly the thing I spent a decade telling people not to do. This was an experiment. Experiments are allowed to be dirty.)

And quite quickly it became clear that plain .NET is, as expected, too slow for this. Not because C# is slow at reading files or walking graphs - the IO and the algorithms are perfectly fine. What kills it is the startup cost.

By default, .NET compiles your code to an intermediate language, and a JIT (just-in-time) compiler turns that into machine code while the program runs. That needs the .NET runtime to be present, started, and then fed with your code every single time. For a long-running server, nobody cares. For a bundler that you start, let finish in a few hundred milliseconds, and then throw away - it's the whole game.

Step 2: To Beat Native, You Need Native

The experiment wasn't over, though, because .NET has an answer for that: AoT (ahead-of-time) compilation. Instead of shipping intermediate language and compiling at runtime, you compile everything to machine code beforehand. The runtime is fused into the binary, there is no JIT, and the program starts right away.

JIT vs. AoT: plain .NET starts the runtime, loads assemblies and JIT-compiles code before the actual work begins, while the AoT binary starts and goes straight to the actual work

And... it worked. This proved that the IO performance is truly native and that C#/.NET is in the same ballpark as Rust, Zig, or Go for this kind of tool. (There is still a garbage collector in the room, so it is not identical - but the part that kills small tools, the startup, was gone.)

AoT does come with its own challenges. Some libraries cannot be used or need work to be AoT-compatible, and you lose some of the dynamic flexibility that .NET is known for - no runtime reflection, for instance. But for a tool like this, it's a deal I'm happy to take.

starts instantly, no warm-up

Step 3: Real Parsing, Borrowed Parts

Armed with that insight, I replaced the regular expressions with actual code understanding. For the JavaScript side I used a third-party parsing library (Acornima). For HTML and CSS I used my own project, AngleSharp (which, I'm happy to report, is still doing great - it gets a second career here).

The result was a bundler fast enough to compete with the established tools, such as esbuild or rspack - at least on the benchmark projects I used back then. It could only handle plain JavaScript, though. No TypeScript, no source maps, no real configuration. I published the experiment, called it done, and moved on.

For quite a while that's where I saw the project: done. A successful experiment, a proof that the idea works, a repo with a README and some benchmark numbers. The missing features (TypeScript above all) would have required forking the parsing library, and I only wanted to do that "if there is enough buzz around the project".

Plot twist: it wasn't buzz that got Netpack moving again.

Step 4: Enter the AI Pair Programmer

With the advancements in AI-assisted software development, I took another look. And I realized that many of the missing things - full TypeScript support being the biggest one - could now be added easily.

Here's what made the difference: the AI (mostly Claude) has sufficient understanding of the project to make modifications within the given architecture. That's the key part. The original architecture of the bundler stayed intact. The AI doesn't redesign the thing - it adds missing features and takes care of important bug fixes, inside the structure that was already there.

That's not luck, by the way, it's a bit of homework. The repository contains a CLAUDE.md that explains the architecture and the non-obvious conventions: how the graph is built, how output formats and platforms plug in, that everything has to stay AoT-safe (no reflection, source-generated JSON), and even which names look like they need "tidying" but must never be touched because they hold the NuGet / AoT story together. There's an xUnit test suite with its own conventions, and the file explicitly tells the AI not to claim that tests pass when it couldn't run them. Good rules for humans, too.

The results speak for themselves. The native toolchain today includes a hand-written JavaScript / TypeScript / JSX tokenizer, parser, printer, minifier, and tree-shaker - no Acornima, Babel, or SWC involved. Compare the numbers: in January 2025 I was debating whether the project deserved a fork of someone else's parser. Today it has its own.

Speedrun!

And the pace went from "weekend experiment" to "release train":

Netpack timeline: the experiment announced on 9 Jan 2025, then - after about 18 months and a switch from a month-scale to a day-scale axis - fifteen releases from v0.1.0 on 10 July to v0.8.2 on 19 August 2026, with the .NET libraries arriving on NuGet on 24 July and a planned 1.0 at the end of 2026

Note that the axis switches from months to days after the break - there's simply no other way to fit it on a screen. The first GitHub release (0.1.0) was on July 10th, 2026. By August 19th we were at 0.8.2. That's fifteen versions in under six weeks, including days with two or even three releases. My commit history now reads like a speedrun.

Not everything on that timeline is mine, either. Since version 0.6.0, there are also contributions from the outside - most notably from mehrabix, who contributed a whole range of things: the plugin hook system, improved watch mode and incremental builds, asset inlining, the preview command, .env support, better error messages with source snippets, CSS code splitting, split-chunks grouping, and more. A massive thank you!

How It Works

Whatever you throw at it, the shape of a build is always the same: Netpack parses the entry, follows what it references to build a module graph, groups that graph into chunks (pulling shared code into its own chunk), and renders each chunk to disk.

Netpack build pipeline: an entry (HTML, JS or TS) is parsed, followed into a module graph, grouped into chunks, and emitted; all inside one AoT-compiled native binary, with an opt-in Node bridge for Sass, LESS, PostCSS, Svelte, Solid, and codegen, and native C# handling of TypeScript, JSX, CSS, Vue, Astro, and images

A few details worth knowing:

  • Most things are handled natively in C#: TypeScript, JSX, CSS, CSS Modules, images (optimized with SkiaSharp in the CLI), JSON, HTML, and even the Vue and Astro compilers - no Node.js round trip required.
  • A few things have a canonical compiler that lives in the JavaScript world: Sass, LESS, PostCSS (including Tailwind), Svelte, Solid, and .codegen files. For those, Netpack has an opt-in Node bridge that talks to your locally installed packages. If you don't use them, you don't need Node.js for the build. (Yes, the .NET bundler sometimes calls Node. I see the irony too.)
  • The dev server watches the file system, rebuilds on change, and hot-swaps modules where it can - including React Fast Refresh if react-refresh is installed. If it can't, it falls back to a full reload.
  • Some things are a bit unusual, and I like them: if your HTML entry point contains an import map, the entries in that map are automatically treated as externals. And if you mark dependencies as --shared, Netpack builds them as their own chunks and wires them into an import map for you.

Getting Started

You install it from npm. The wrapper package picks the right native binary for your platform (Windows x64 / ARM64, Linux x64 / ARM64, macOS ARM64):

npm i -D netpack
Enter fullscreen mode Exit fullscreen mode

Then there are three main commands:

# one-shot production build
npx netpack bundle src/index.html --minify --sourcemap

# dev server with HMR (default port 1234)
npx netpack serve src/index.html

# inspect what ended up in your bundle
npx netpack analyze src/index.html --interactive
Enter fullscreen mode Exit fullscreen mode

You don't need an HTML file; a script is a fine entry point, too: npx netpack bundle src/main.tsx. Your package.json scripts will look like they always do:

{
  "scripts": {
    "dev": "netpack serve src/index.html",
    "build": "netpack bundle src/index.html --minify --sourcemap",
    "analyze": "netpack analyze src/index.html --interactive"
  }
}
Enter fullscreen mode Exit fullscreen mode

Want React and friends as separate, cacheable chunks wired up through an import map? Mark them as shared:

npx netpack bundle src/index.html --shared react --shared react-dom
Enter fullscreen mode Exit fullscreen mode

The analyze command deserves a shout-out: besides the interactive graph explorer, it audits the dependencies that actually made it into your bundle against known vulnerabilities and gives suggestions for where your chunks could be split more efficiently. (Tree-shaken packages are not audited - only what ships.)

For module-federation fans (hi, Piral people): a federation.json entry point gives you Module Federation and Native Federation remotes. Netpack also supports ESM, CommonJS, UMD, and SystemJS output, plus web, Node, and Deno targets. Everything is covered in the documentation.

Using It from .NET

Here's the part where being written in C# pays off. Netpack is not just a CLI - there are three ways in:

Netpack from .NET: the NetPack.Core engine at the center, used by the netpack npm CLI, the NetPack.Build MSBuild task, the Node.js API, and the parser as a library, with ASP.NET Core apps and Cake / Fallout build scripts as what the library makes possible

1. The MSBuild task. NetPack.Build bundles your web entry point into wwwroot as part of dotnet build / dotnet publish:

dotnet add package NetPack.Build
Enter fullscreen mode Exit fullscreen mode
<PropertyGroup>
  <NetpackEntry>ClientApp/src/index.html</NetpackEntry>
</PropertyGroup>
Enter fullscreen mode Exit fullscreen mode
dotnet build
# -> wwwroot/index.html, wwwroot/index.js, wwwroot/styles.css, ...
Enter fullscreen mode Exit fullscreen mode

That's it - no npm run build step to remember, no extra pipeline stage. There are more MSBuild properties (NetpackMinify, NetpackSourceMaps, NetpackFormat, NetpackEntryNames, ...) and an item group for externals. It's built on the core library and ships a pure-managed image processor, so there's no native dependency to worry about.

2. The library. NetPack.Core is the bundler engine as a managed library. Its only dependencies are AngleSharp and its CSS companion (see, I told you it's still doing great):

using NetPack;

// in memory ...
var result = await Bundler.BundleAsync("src/index.html", new BundleOptions
{
    Minify = true,
    Platform = Platform.Web,
    Format = ModuleFormat.Esm,
});

byte[] indexHtml = result.Outputs["index.html"];

// ... or straight to a directory
await Bundler.WriteToDirectoryAsync("src/index.html", "dist",
    new BundleOptions { Minify = true, SourceMaps = true });
Enter fullscreen mode Exit fullscreen mode

3. The parser, as a library. The TypeScript / JSX front-end is public, too - so you can parse, transform, and print without bundling anything. For instance, you can turn TypeScript into JavaScript from C#:

using NetPack.Syntax;
using NetPack.Syntax.Printer;

var module = Parser.ParseModule("const x: number = 1;", "in.ts");
var js = JsPrinter.Print(module); // -> "const x = 1;"
Enter fullscreen mode Exit fullscreen mode

Why does that matter? Because things that were awkward when your bundler lives in another ecosystem suddenly become straightforward. A bundler that is just a .NET library can be part of an ASP.NET Core web server, or of a build pipeline based on MSBuild, Cake, or Fallout - no child process, no JSON handshake, no "please have Node.js installed on the build agent" in your onboarding doc. It can post-process and optimize the output of ASP.NET Core and Blazor applications from within the same process. Many optimizations and use cases become possible that were difficult before. (The two dashed boxes in the diagram are exactly that: things the library makes possible, which I'm still exploring, versus the solid ones that exist today.)

finally, they're speaking my language

How Fast Is It, Really?

Time for the honest part. In January 2025, the numbers looked fantastic: netpack finished the small project in 418 ms, esbuild needed 670 ms, rspack roughly 910 ms, and Vite 1.66 s. (That table is, in fact, still on the website. I should update that.) But the world didn't stand still. esbuild got faster, rspack got a major version, and Vite switched to a Rust-based bundler (Rolldown) with version 8.

Here are the current numbers from the repository's performance.md - netpack 0.6.0 against the latest versions of the others, measured with hyperfine on the same test projects (the table was last run for 0.6.0 - the project has moved on to 0.8.2 since):

Benchmark chart: netpack 0.6.0 is fastest on library builds at 141 ms, ties esbuild on the small project at 240 ms, beats esbuild on medium and large projects (637 and 595 ms vs. 819 and 895 ms), but rspack 2.1.5 and Vite 8.1.5 are faster on medium and large projects

My reading of that chart:

  • Library builds: netpack wins (141 ms vs. 150 ms for esbuild).
  • Small project: a dead heat with esbuild (240 ms vs. 238 ms), both ahead of rspack and Vite.
  • Medium and large projects: netpack comfortably beats esbuild, but rspack 2 and Vite 8 are faster (around 450 ms vs. 595 ms on the large project).

So: still in the same ballpark as the native tools, and the original thesis holds - but no, it doesn't "beat everyone" anymore. As always, take benchmarks (especially the ones the author runs himself) with a healthy dose of salt. And note that features like source maps and tree-shaking cost time, too - more features, more work to do per build.

Where It Shines

  • Native startup. An AoT-compiled binary that starts instantly and behaves the same whether it bundles two files or a large app.
  • Zero-config and batteries-included. Same entry-point convention as Vite or Parcel, with TypeScript, JSX, CSS Modules, Sass / LESS / PostCSS, images, Vue, Astro, Svelte, and Solid on board.
  • A real toolchain, not a wrapper. Hand-written tokenizer, parser, printer, minifier, and tree-shaker - all in readable C#. (Arguably more readable than the Rust equivalent. I said arguably.)
  • .NET-native. Library, MSBuild task, and parser - all usable without leaving the .NET world.
  • Smart defaults for modern web. Import maps as externals, --shared dependencies, Module and Native Federation.
  • A genuinely useful analyzer, with dependency audits and optimization suggestions.

Where It Struggles

  • It's pre-1.0 and experimental. The README still carries a warning that it's not production-ready, and it's still true: the chance that it works end-to-end for your project today isn't as high as I'd like. Please test it on side projects first.
  • The ecosystem is the elephant in the room. Vite is downloaded around 65 million times a week. Its plugin ecosystem, its documentation, and its integration with every framework under the sun can't be replicated - and I'm not going to pretend otherwise. Netpack has hooks (inspired by rspack), but don't expect to drop in your existing Vite plugins.
  • Not every framework is there. Angular is not supported (yet). Svelte and Solid compile through the Node bridge; Vue and Astro are native, but with limitations documented in their respective pages.
  • Not every platform is there. There are binaries for Windows x64 / ARM64, Linux x64 / ARM64, and macOS ARM64. macOS x64 and Windows x86 are still missing.
  • AoT has a price. No reflection, source-generated JSON, and a careful eye on every dependency. That's good for startup, but it limits what libraries I can pull in.
  • You still need Node.js for some things (Sass, LESS, PostCSS, Svelte, Solid, .codegen). Plain JS / TS / JSX / CSS / HTML bundling needs nothing extra.
  • Bundler, not type checker. As with the other bundlers in this space, I'd keep running tsc in your CI for actual type checking - Netpack's job is parsing TypeScript and stripping the types, not judging them.

Community, Such As It Is

I want to be upfront about this one: Netpack is very early. At the time of writing the repository has 12 stars - so if you've read this far, you could be a statistically significant part of the community.

That also means every contribution counts, and there's plenty of low-hanging fruit: macOS x64 and Windows x86 binaries, framework support (hello, Angular), docs, bug reports from real projects, and benchmarks on your own machines. If you like where this is going, star it, try it, open an issue, or sponsor the project 🍻. The code is MIT licensed.

What's Next for Netpack

First things first: 1.0. The project is still pre-1.0, but I'm positive that I'll get this done by the end of the year - together with a proper release to the community. My 1.0 homework list: closing gaps in the supported scenarios (looking at you, Angular), settling the APIs - including the .NET ones - and bringing documentation and benchmarks up to date.

I have no illusions, though. The existing bundlers - most importantly Vite as the "meta bundler" - are far too prominent and too well integrated with the ecosystem to be replaced or discarded. Netpack is not going to be your next Vite, and I'm not trying to make it one.

What makes Netpack shine - and where I see its future - is the integration into .NET. As Netpack is .NET "native" (yes, the application itself is also an AoT artifact, but I mean something else here: it's based on C#/.NET and can be used as a library in .NET projects), it can be part of ASP.NET Core web servers or build pipelines based on MSBuild, Cake, or Fallout. In any of these cases, many optimizations and use cases are now possible that would have been difficult beforehand.


That's the Netpack story - from a regular-expression-powered experiment on a winter vacation, via the realization that "to beat native you need native", to a native, AoT-compiled bundler that got fifteen releases in six weeks with a little help from an AI pair programmer. If you'd like to poke around, here are some links:

Next up in this series: another project, another origin story. Stay tuned.

Top comments (2)

Collapse
 
koda2026 profile image
Harun - solo dev •

the "to beat native, you need native" realization is huge.

i build on the complete opposite end of the spectrum (a 92kb single-file vanilla js app with literally zero build step so it loads in 400ms on mobile 3g), but the obsession with eliminating "warm-up" costs is exactly the same. whether it's a .net aot binary or a raw edge-worker script, startup latency kills ux.

also, massive respect for the honesty on the benchmarks. admitting that vite's 65m weekly downloads can't be replicated overnight, while still shipping a hand-written ts parser via ai pair-programming inside a strict aot architecture... that's how real oss is built.

starred the repo. the msbuild integration is a genius seam. good luck with the 1.0 push! 🐯

Collapse
 
florianrappl profile image
Florian Rappl •

Thanks much appreciated!

Also I like the vanilla JS single-file app. I think this is super useful and underutilized - we should not forgot that bundling is not a goal, but just a helper.