Quick answer: Static batching is computed at scene build time. Prefabs spawned at runtime are not pre-batched. Call StaticBatchingUtility.Combine after spawning, or switch to GPU Instancing on the material for dynamic transforms.
Here is how to fix Unity static batching that boasts a small draw-call count for your scene-placed props but explodes with every prefab you spawn at runtime. Procedurally generated rocks add 800 draw calls. The static flag is on. Frame Debugger shows them un-batched. Static batching only happens at build/load time; you have to ask for it explicitly when spawning.
The Symptom
A scene with 1000 hand-placed rocks shows 12 draw calls. The same scene with 1000 runtime-spawned rocks (identical prefab) shows 1000 draw calls. The static flag on the prefab is on. Stats panel reports zero static batching activity for the runtime ones.
What Causes This
Static batching is build-time. The static batcher walks the scene at build/load and combines static-flagged meshes into vertex buffers. Runtime spawns happen after this pass.
Static flag set on prefab does not auto-batch. The flag’s only effect at runtime is to enable future StaticBatchingUtility.Combine. It does not retroactively batch the prefab.
Material with shared tiling. Even after combine, materials with per-instance MaterialPropertyBlock break batching. All instances must use the same material asset.
Different mesh assets. Static batching combines meshes regardless of source asset, but at the cost of vertex memory equal to the sum. Different meshes still produce one draw call but one big vertex buffer.
The Fix
Step 1: Combine after batch-spawning.
using UnityEngine;
public class RockSpawner : MonoBehaviour
{
[SerializeField] private GameObject rockPrefab;
[SerializeField] private int count = 500;
void Start()
{
var root = new GameObject("RocksRoot");
for (int i = 0; i < count; i++)
{
Vector3 pos = Random.insideUnitSphere * 100f;
Instantiate(rockPrefab, pos, Quaternion.identity, root.transform);
}
// One call batches all children
StaticBatchingUtility.Combine(root);
}
}
Once combined, the children become non-movable static geometry. Trying to move them produces visible artifacts.
Step 2: For movable instances, enable GPU Instancing.
// On the material, check Enable GPU Instancing
// All instances using this material auto-batch when:
// - same mesh
// - same material asset (not MaterialPropertyBlock-customized)
// - within Camera frustum
GPU Instancing supports dynamic transforms (you can move instances) at the cost of one extra GPU descriptor table. Excellent for foliage, particles, debris.
Step 3: Avoid MaterialPropertyBlock for batching. If you set per-instance color via MaterialPropertyBlock, batching breaks. Instead, use a property accessed via instance data on a GPU-instanced material:
// In shader / Shader Graph:
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
UNITY_INSTANCING_BUFFER_END(Props)
// In code:
MaterialPropertyBlock mpb = new MaterialPropertyBlock();
mpb.SetColor("_Color", randomColor);
renderer.SetPropertyBlock(mpb);
This works with GPU Instancing because the shader pulls per-instance values from a GPU buffer.
Step 4: Verify with Frame Debugger. Window → Analysis → Frame Debugger. Look at draw calls. After Combine, you should see fewer calls labeled Static Batched. With GPU Instancing, you see Draw Mesh (Instanced).
Step 5: Keep an eye on memory. Static batching duplicates vertex data into a combined buffer. 1000 rocks of 500 verts each = 500k extra verts. For mobile, GPU Instancing is usually better because vertex data stays single-instance.
“Static batching is for static. Runtime spawns need explicit Combine or GPU Instancing.”
Related Issues
For draw call optimization in general, see Optimizing Draw Calls. For mesh combining issues, see CombineMeshes Missing UVs.
Combine for static spawns. GPU Instancing for movable instances. Frame Debugger to confirm.