# Static Code Generation vs Reflection in protobuf-go-lite: Performance Benefits Explained

> Discover performance benefits of static code generation over reflection in protobuf-go-lite. Achieve faster execution, reduced allocations, smaller binaries, and TinyGo compatibility.

- Repository: [Aperture Robotics/protobuf-go-lite](https://github.com/aperturerobotics/protobuf-go-lite)
- Tags: performance
- Published: 2026-02-25

---

**protobuf-go-lite eliminates runtime reflection by generating fully unrolled Go code for every protobuf operation, resulting in faster execution, lower memory allocations, smaller binaries, and full TinyGo compatibility.**

The `aperturerobotics/protobuf-go-lite` repository provides a Protocol Buffers implementation for Go that replaces the reflective runtime of `google.golang.org/protobuf` with compile-time code generation. Unlike the standard library, which relies on `protoreflect` types at runtime to determine field wire formats and perform serialization, protobuf-go-lite generates static methods like `SizeVT`, `MarshalVT`, and `EqualVT` that contain fully unrolled logic specific to each message type.

## How Static Code Generation Works in protobuf-go-lite

The `cmd/protoc-gen-go-lite` tool walks the protobuf descriptor tree and emits Go source files for each message. The generator in [`generator/generator.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/generator/generator.go) orchestrates feature modules located in `features/*`, with each module containing templates that emit concrete methods for specific operations.

For example, [`features/size/size.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/features/size/size.go) generates the `SizeVT()` method, while [`features/marshal/marshal.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/features/marshal/marshal.go) generates `MarshalVT*` methods. All `protoreflect` types are used only during generation; the resulting code works with native Go types exclusively. This eliminates the per-message cost of looking up field descriptors, computing wire types, and handling generic `proto.Value` objects at runtime.

## Performance Benefits of Static Code Generation

### Faster Runtime Execution

The generated code contains straight-line logic with no reflective lookups. Every operation—size calculation, marshaling, unmarshaling, equality checking, and cloning—is compiled into fully unrolled code that knows each field's type, number, and wire format at compile time. According to the source analysis, the generated helpers are "fully unrolled" and "do not use reflection" for size, marshal, unmarshal, equal, and clone operations.

### Reduced Memory Allocations

The `SizeVT` helper pre-computes the exact buffer size required for a message, allowing `MarshalToSizedBufferVT` to write directly into a pre-allocated slice without intermediate temporary objects. The `MarshalVT` method allocates only once. This contrasts with reflection-based approaches that often require additional allocations for `proto.Value` objects and dynamic buffer resizing. As noted in the README, `SizeVT` pre-computes the exact buffer size, letting `MarshalToSizedBufferVT` write directly.

### Smaller Binary Size

Because the generated code replaces a large reflection engine, the resulting binary contains only the code actually needed for the compiled messages. The static code generation approach eliminates the overhead of the `protoreflect` runtime, resulting in smaller code binaries suitable for constrained environments.

### TinyGo Compatibility

TinyGo has limited reflection capabilities. By eliminating runtime reflection entirely, protobuf-go-lite works everywhere TinyGo runs, making it suitable for WebAssembly and microcontrollers where the standard protobuf library cannot operate.

### Predictable Field Ordering

The `MarshalVTStrict` method guarantees fields are written in the order declared in the `.proto` file. This strict ordering eliminates any hidden logic that reflection would need to enforce at runtime, providing deterministic wire format output.

### Zero-Copy JSON Unmarshalling

When using the optional `unmarshal_unsafe` feature, `UnmarshalVTUnsafe` uses unsafe casts to avoid copying byte slices when reading JSON. This zero-copy approach performs fewer allocations than the safe reflection-based alternatives.

## Generated Code Examples

### SizeVT Method

The `SizeVT` method generated in `*.pb.go` files calculates the exact wire size without reflection:

```go
func (m *MyMessage) SizeVT() (n int) {
    if m == nil { return 0 }
    var l int
    // field 1: int32 (varint)
    if m.Foo != 0 {
        n += 1 + protowire.SizeVarint(uint64(m.Foo)) // key size (1) + varint size
    }
    // field 2: repeated string
    for _, s := range m.Bar {
        l = len(s)
        n += 1 + protowire.SizeVarint(uint64(l)) + l // key + length prefix + data
    }
    n += len(m.unknownFields)
    return n
}

```

This code is generated by [`features/size/size.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/features/size/size.go), which emits fully unrolled size calculations using `KeySize` and `ProtoWireType` helpers defined in [`generator/helpers.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/generator/helpers.go).

### MarshalToSizedBufferVT Method

The marshaling logic writes directly to pre-allocated buffers in reverse order:

```go
func (m *MyMessage) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
    i := len(dAtA)
    // unknown fields
    i -= len(m.unknownFields); copy(dAtA[i:], m.unknownFields)
    // field 2: repeated string (reverse order for sized buffer)
    for iNdEx := len(m.Bar)-1; iNdEx >= 0; iNdEx-- {
        s := m.Bar[iNdEx]
        i -= len(s); copy(dAtA[i:], s)
        i = protowire.EncodeVarint(dAtA, i, uint64(len(s)))
        i = protowire.EncodeVarint(dAtA, i, 2<<3|protowire.BytesType) // key
    }
    // field 1: int32
    if m.Foo != 0 {
        i = protowire.EncodeVarint(dAtA, i, uint64(m.Foo))
        i = protowire.EncodeVarint(dAtA, i, 1<<3|protowire.VarintType) // key
    }
    return len(dAtA) - i, nil
}

```

This implementation is generated by [`features/marshal/marshal.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/features/marshal/marshal.go), utilizing the `reverseListRange` logic and direct `EncodeVarint` calls from [`generator/helpers.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/generator/helpers.go) to avoid runtime type switches.

### EqualVT Method

Equality checking performs direct field comparisons without reflection:

```go
func (m *MyMessage) EqualVT(that *MyMessage) bool {
    if m == nil || that == nil { return m == that }
    if m.Foo != that.Foo { return false }
    if len(m.Bar) != len(that.Bar) { return false }
    for i := range m.Bar {
        if m.Bar[i] != that.Bar[i] { return false }
    }
    return bytes.Equal(m.unknownFields, that.unknownFields)
}

```

The [`features/equal/equal.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/features/equal/equal.go) module generates this logic by walking each field and emitting direct comparisons, avoiding the expensive `proto.Equal` reflection path used by the standard library.

## Key Source Files and Architecture

The static code generation architecture is implemented across several key packages:

| Feature | Primary Source File (Generated Code) | Generator Implementation |
|---|---|---|
| Size calculation | `*.pb.go` – `SizeVT()` method | [`features/size/size.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/features/size/size.go) |
| Marshal (regular & strict) | `*.pb.go` – `MarshalVT*` methods | [`features/marshal/marshal.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/features/marshal/marshal.go) |
| Unmarshal | `*.pb.go` – `UnmarshalVT` / `UnmarshalVTUnsafe` | [`features/unmarshal/unmarshal.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/features/unmarshal/unmarshal.go) |
| Equality | `*.pb.go` – `EqualVT()` | [`features/equal/equal.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/features/equal/equal.go) |
| Clone | `*.pb.go` – `CloneVT()` | [`features/clone/clone.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/features/clone/clone.go) |
| JSON (fastjson) | `*.pb.go` – `MarshalJSON` / `UnmarshalJSON` | [`features/json/json.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/features/json/json.go) |
| Helper utilities | [`generator/helpers.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/generator/helpers.go) (e.g., `KeySize`, `ProtoWireType`) | – |
| Core generator driver | [`generator/generator.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/generator/generator.go) (orchestration) | – |
| CLI plugin | [`cmd/protoc-gen-go-lite/main.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/cmd/protoc-gen-go-lite/main.go) | – |

The `cmd/protoc-gen-go-lite` entry point drives the generation process, while modules in `features/*` contain the templates that emit concrete implementations for each operation. All `protoreflect` usage is confined to generation time; the resulting `*.pb.go` files contain only native Go types and operations.

## Summary

- **protobuf-go-lite** replaces runtime reflection with compile-time code generation, producing static methods for every protobuf operation.
- **Faster execution** results from fully unrolled logic in `SizeVT`, `MarshalVT`, and `UnmarshalVT` that knows wire types at compile time.
- **Lower allocations** occur because `SizeVT` pre-calculates exact buffer sizes, enabling single-allocation marshaling via `MarshalToSizedBufferVT`.
- **Smaller binaries** are achieved by eliminating the `protoreflect` runtime engine and including only necessary generated code.
- **TinyGo and WebAssembly support** is enabled by removing all reflection dependencies.
- **Predictable strict ordering** via `MarshalVTStrict` guarantees field order without runtime logic.
- **Zero-copy JSON** unmarshalling via `UnmarshalVTUnsafe` avoids slice copying using unsafe casts.

## Frequently Asked Questions

### How does protobuf-go-lite achieve better performance than google.golang.org/protobuf?

protobuf-go-lite generates fully unrolled static methods like `SizeVT`, `MarshalVT`, and `EqualVT` that contain explicit logic for each field's type and wire format. This eliminates the runtime descriptor lookups, type switches, and `proto.Value` allocations that the standard library performs using reflection. The generated code executes as straight-line native Go operations without any reflective overhead.

### Can I use protobuf-go-lite with TinyGo for WebAssembly or embedded targets?

Yes. TinyGo has limited reflection capabilities that prevent the standard `google.golang.org/protobuf` library from functioning. Because protobuf-go-lite generates static code in `*.pb.go` files that use only native Go types and operations, it works everywhere TinyGo runs, including WebAssembly and microcontroller environments where reflection is unavailable.

### What is the difference between MarshalVT and MarshalVTStrict?

`MarshalVT` marshals fields in an order that may be optimized for performance. `MarshalVTStrict` guarantees that fields are written to the wire in the strict order defined by their field numbers in the `.proto` file. This deterministic ordering is enforced by the generated code structure itself, eliminating any hidden runtime logic that reflection-based implementations would need to enforce ordering constraints.

### Does static code generation increase binary size?

No. Static code generation actually reduces binary size compared to the standard library. While you generate additional methods for each message, you eliminate the large `protoreflect` runtime engine and descriptor registries that the standard library requires. The resulting binary contains only the specific serialization logic needed for your compiled messages, producing smaller overall binaries suitable for constrained deployments.