As Pavel is suggesting, there are plenty of things to consider, some of them at ‘build-time’ (i.e. If you need to update some buildings afterwards, having them separeted would avoid having to re-export the entire city again), others in ‘download-time’ (some 3d formats include a distribution mechanism suited for this specific challenge, like 3D-Tiles), others in ‘execution-time’ (space/object indexing/partinioning systems like octree and BVH will help you in avoiding rendering what is not visible on-camera), and all the combinations between them.
You can read about those things in advance, at least to get the idea of each. But I would strongly suggest to start by defining first what is the problem you are gonna evaluate those solutions against: i.e. what size is need for optimizations? In other words, you need to know very precisely the total footprint budget you have for spending, the amount of triangles and textures you can use before crashing your app. And that is bounded to the OS - hardware / device you are aiming to, for instance:
- WebGL Report gives you a bunch of statics about your device capabilities. You can have a look at the textures section, for example.
- If you want details of the rendering context within your application, you can always pull data from the three.js WebGLRenderer. capabilities object, which returns values from the actual three.js rev / rendering context (the same your app will use)
- The same for vertices/triangles, you need to find out what is the total amount of triangles you can use per frame, this will lead you to clarify the max drawcalls per frame. Even trial and error coud be handy here, you can produce your own benchmarks ‘datasets’ for you to test this.
Once you have some idea of your specs limitations, you can start fiddling code or testing things. Maybe you just need a good enough texture compression schema.
Good luck!