Clarification on [lazy = true] behavior: Standard Java vs. Java Lite

29 views
Skip to first unread message

Jayaprakash Jp

unread,
Sep 18, 2026, 8:11:32 AM (6 days ago) Sep 18
to Protocol Buffers

Hi everyone,

I’m looking for some clarification regarding the [lazy = true] field option and how it translates to generated Java code, as I've noticed some behavior reminiscent of older discussions (like issue #3601 - https://github.com/protocolbuffers/protobuf/issues/3601).

Currently, standard protoc syntactically accepts the [lazy = true] annotation on message fields without any warnings. However, the resulting files generated via --java_out completely ignore this directive—the generated code falls back to standard eager parsing and storage, and LazyField is nowhere to be found in the internal implementation.

In contrast, I found that using the separate Java Lite plugin does successfully generate the expected LazyField wrappers. I am able to get the lazy behavior working by passing these additional options to the compiler:
--plugin=protoc-gen-javalite=<path/to/protoc-gen-javalite> \
--javalite_out=<output_dir>

Could the maintainers clarify a few things regarding this?

  1. Standard Java: Is the omission of LazyField in the standard Java generation intentional, or is [lazy = true] strictly treated as an unsupported hint for standard Java builds?

  2. Java Lite: Are we actively supporting [lazy = true] via the javalite plugin, or is this considered legacy/experimental behavior that might be removed?

I want to ensure I'm optimizing deserialization using a supported path before committing to the Java Lite implementation.

Thanks for the insights!
- JP

Em Rauch

unread,
Sep 18, 2026, 8:38:04 AM (6 days ago) Sep 18
to Jayaprakash Jp, Protocol Buffers
Yeah, in general the [lazy=true] option is soft-deprecated and not honored in most of our runtimes, which I think changed in both Java and JavaLite sometime around 2017.

I think your repro is likely only working because you are using some `protoc-gen-javalite` plugin that is ancient, before we made the decision that honoring lite is net harmful. protoc itself has java and javalite built in without plugins, like `protoc --java_out=lite:path/to/output/dir path/to/file.proto` and I believe you will find it will ignore the lazy option.

The context is that we found that [lazy=true] was a net performance regression when honored at scale: if you tag a field as lazy and still use it, its slower than if the field is not lazy (otherwise we would just do lazy all the time). People generally misidentify when a field should or shouldn't be lazy. For example, they might identify that it should be on based on its use on one server where the field was cold, but then apply that logic to a second server where the field is hot. Or, they correctly identify it but then the server code changes. Or, they measure with microbenchmark (which is actually extremely hard to do accurately), leading them to wrongly set the option always.

In C++ internally we make the decision profile-driven only because humans can't accurately make the decision, but its fairly difficult for us to offer profile-driven behaviors to open source users unfortunately. In Java internally we just don't do lazy fields (not even profile-driven), though there are some cases where extensions are lazy (as noted on the linked 2017 issue).

I think you're on a server, but specifically for Lite, it's additionally 'harmful' for another nonobvious reason: Lite is meant to be used on mobile, and on mobile people generally perform the parse on the background thread but then often read the contents on the UI thread. Any parsing work deferred causes jank, where some "+3ms on a background thread" is almost always worth avoiding "20% chance of +4ms on UI thread" instead (the cost of deferring is not symmetrical).

In general, Google's own servers and clients using Java are able to achieve high performance without leaning on Lazy too much, though it does sometimes create oddities where trimming schemas can be a bigger improvement than it should be.

If you have some specific performance concerns with JavaProto, feel free to open a github issue with your findings.

Thanks!

--
You received this message because you are subscribed to the Google Groups "Protocol Buffers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to protobuf+u...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/protobuf/b536dbbf-b531-4aee-8f6b-6bbef6b585f4n%40googlegroups.com.

Jayaprakash Jp

unread,
Sep 18, 2026, 9:33:45 AM (6 days ago) Sep 18
to Protocol Buffers
Thanks for the context — that rules out [lazy=true] for us.

To clarify our case: it's not a per-server hot/cold guess — it's a stable structural pattern. We read protos via REST, convert to POJO, then serialize to JSON, and <5% of requests ever touch the deeper nested fields we're evaluating this for.

Since [lazy=true] isn't honored, our fallback is: store those rarely-used submessages as bytes (wire-compatible with embedded message, so no breaking migration), parse on-demand via parseFrom(), and explicitly cache the result per-request to avoid repeat-parse cost.

Two questions before we roll this out at scale (~100B messages already stored, ~100M requests/day):

Is "manual bytes + on-demand parse + explicit caching" an acceptable pattern at this volume, or are there pitfalls that wouldn't surface in a smaller benchmark (we tested at 200K messages)?
The UI-thread-jank concern you mentioned was Lite/mobile-specific (deferred parse crossing threads). Since our parsing happens and completes within a single server request (no cross-thread deferral), does that risk still apply, or is it specific to the mobile deferred-parsing pattern?
Just want to confirm we're building on a supported pattern rather than something already known to be problematic.

Thanks,
- JP

Em Rauch

unread,
Sep 18, 2026, 9:44:00 AM (6 days ago) Sep 18
to Jayaprakash Jp, Protocol Buffers
Yep: switching the field from a message type to a bytes type is perfectly sensible thing to do to try to improve performance edge cases like that (you can actually safely even do it in place if you're only using the field in binary format and not in TextProto or ProtoJSON, if you're using the latter formats anywhere you will need to make a new field and migrate).

It actually gives you more control/power than the built-in lazy in many ways, though is ergonomically more awkward (since you have to have the side cache yourself instead of it being updated internally).

For a direct comparison, `google.protobuf.Any` is just that: the message definition just has two fields of a `string` of the message name and a `bytes` field of a binary encoded protobuf payload, and all that the Any::Pack/Unpack APIs do is parseFrom() on those bytes on demand. So if you know the given field is only one message, using Any is the same but with a little more overhead of also wire encoding the message name next to the bytes, which is 'worse' but conversely allows for things like DebugString ergonomics to be better.

does that risk still apply, or is it specific to the mobile deferred-parsing pattern?

Nope. It's still true that if you're going to touch every field that you're better off just parsing it up front, but the extra case of "actually there's cases where deferring parse can be worse even if the overall parse" is really more of a clientside usecase topic.

Reply all
Reply to author
Forward
0 new messages