Reuse them. Unity has a pool built in.

The problem
New every shot, gone every hit.
void Fire(){ var b = Instantiate( bulletPrefab );} // in Bullet.cs, on hitDestroy( gameObject );
Instantiate and Destroy are expensive, and the garbage they leave causes stutters.
The fix
Borrow it. Give it back.
A pool keeps finished bullets switched off, and hands them out again instead of making new ones.
Get()→use→Release()
The fix in code
UnityEngine.Pool.
ObjectPool<Bullet> _pool; void Awake(){ _pool = new ObjectPool<Bullet>( () => Instantiate( bulletPrefab ), b => b.gameObject.SetActive( true ), b => b.gameObject.SetActive( false ) );} void Fire() => _pool.Get();
Tell it how to create, take and return a bullet. Get hands one out, making a new one only if none are free.
On hit
Release, don't Destroy.
// Bullet needs a reference back to its poolvoid OnHit(){ pool.Release( this );}
Release switches it off and puts it back for the next shot.
The rule
Spawn a lot? Pool it.
- Take one
- pool.Get()
- Done with it
- pool.Release( b )
- Destroy
- never, for pooled objects