In my experience, the number one way to get good performance is by using caches properly.

Preferably cache as close to the consumer as possible, for example using a cdn.

If you can't afford/use a cdn, then enable equivalent caching in your load balancer. (Nginx, haproxy, probably Apache, can all cache very well)

I have only experience with nginx, and there you can configure the caches to serve stale data if upstream is overloaded, and you can set it to refresh cache in background. It helps alot!

If you deliver a resource from nginx memory cache, there is not much more than transfer times in play!

In your services, precalculate as much as possible.

For example, don't gzip/brotly/compress on the fly, do it at build time when possible.

Same with dynamic data from a backend that needs to be preprocessed. If possible, do the preprocessing "outside" of the request handler, in advance.

When it comes to actual performnce of services, in my experience go and java delivers a magnitude better raw power then nodejs. (It might be me.)

Unfortunately, if you need to do eg serverside rendering of js templating system (vue, react, etc) youre pretty much doomed to use nodejs for serverside.

In this case, make sure the serverside generates data that is cacheable and "same for large chunks of users". This means avoiding cookies on requests that don't need them, since cookies.in general renders the payload uncatchable.

Caching is also a reason to avoid graphql. Imo, when you make graphql queries cacheable, you lost all benefits of graphql.

When it comes to data used in backend services, there as well you can use caching layers in smart ways. For example, delivering product data might require up-to-date stock info, but lots of other info (name, description) is changed rarely.

In this case, cache the stable data longer and the dynamic data more often.

When it comes to horizontal scaling, remember that in-memory caching done in backend services is local to that service instance. So if scale up, you might get more requests to the dB.

When it comes to dB, make good choices and limit heavy joins, transactions, etc in real/request time.

By good choices, I mean: it might be better to store session info in redis than SQL.
I'm currently using an ecom platform where they store everything in SQL dB. Sessions, shopping carts, etc. Needless to say, the dB server needs to be beefy.
They could've easily put sessions and shopping carts in a scalable kv-storage without drawbacks.

If you can avoid sessions (or server state), do it. If you can't, limit their use and size.

Once a clever developer at a previous gig decided to put "recently visited products" in session of user.
It was not optimal. Lots of data going back n forth. Filling memory. Etc.

When processing requests, it might be a good idea to have knowledge of when to use lists, when to use hashmaps, etc, so you don't end up in crazy waste of resources.

If a request takes 10 or 20 ms of CPU makes a big difference if your users/rps increase.

Also, try to make your services horizontally scalable from beginning. Imo, much easier to do right from start than to fix afterwards.

Last, but not least, load test you apps using a tool like jmeter, locust or (possibly preferably) goose.

Have fun :-)

(Edit: name of load test tool)