An approach I use to avoid computing particle positions on the CPU and updating buffers on the GPU is to instead procedurally generate everything on the GPU's shaders.
If you create a buffer of random numbers, and another random number for the particular particle effect instance, you can use these numbers in your shader to to decide, for any given time point, where a particle would and the rest of its state.
And although you are re-using this exact same little VBO for each and every particle system, and many may be visible on-screen at the same time, no two particle systems look the same!
I've used it for 2D flame effects and even for particles that follow moving objects in 3D. There are obvious limitations - particles that react with other things in the scene e.g. bounce off walls - cannot be modelled easily this way.