In general, performance differences are usually negligible as long as they don't turn up during benchmarking (you probably know what people say about premature optimization...).
So, it's mostly a matter of style. Writing a
GraphStage is the lowest level you can implement something in
akka-stream. It's very explicit and somewhat predictable what happens but the amount of code to write can be quite
cumbersome and with a growing number of ports, managing the state space and all the possible interactions can be
quite a challenge.
Wiring up predefined combinators using the GraphDSL is therefore often recommended. In the best case it's just "plug and play". On the other hand, it also introduces new degrees of freedom and non-determinism as elements flow through the stream (each predefined combinator basically runs on its own). If your overall graph contains feedback loops or parts of your pipeline are used only for control messages, then, from experience, the extra degrees of freedom (non-determinism and potentially backpressure that prevents control signals from reaching some stages) start to become a burden in itself. We observed this in the Akka HTTP implementation where we started to replace stream graphs built from predefined combinators with mostly BidiFlows implemented as explicit GraphStages that encapsulate some layer of the implementation stack. This allowed us to get rid of putting control signals on the stream and also of the feedback loops.