That you have to weigh 2 vs. 4 byte lengths is exactly making my point. There is also a case for 1 byte lengths, and for string implementations other than length + pointer, like for example rope datastructure implementations.
My point is that there is not one sensible string implementation, and acting like there was is in many situations trading short term convenience for long term pains in larger projects.
> I know not all strings will be malloc'd but a lot will be. And we seemed to deal with this overhead fine in the 90s?
It's a common but deeply flawed assumption that allocations and lifetimes are so random that every little object should be individually allocated and later deallocated with malloc()/free() or with another generic allocator. I don't use malloc() to allocate string buffers - except in rare situtations of laziness, knowing it will come back to bite me later. Not only performance concerns but also the practical impossibility of matching each malloc() with a free() forbids that. Systems like RAII come to help to solve the latter issue, but I prefer to take the difficulty of matching everything up as an indication that the general approach is too complicated.
Instead I recommend to allocate strings using a fixed field (eg. char buf[16]) on a stack frame, or using a member in a struct, in order not to add any management overhead. Alternatively, for unbounded and dynamically sized strings, it's often a good idea to allocate them using linear allocators. For example, in a GUI renderer, it's a good idea to have a per-frame allocator that collects all allocations in a list of larger chunks, to minimize the allocation overhead to a few chunks (KBs to MBs) that were individually allocated from the system. With this, everything is freed using a single central function call after the frame was renderered and the data is now not needed any longer.