Ooh I love this kind of thing!
I try to keep a mental note of what is considered an "expensive" event, and what is not - There's no list that I know of, but I mostly memorised a lot of it over the many years. Some expensive events being: Changing an object's Z order, collisions (especially if it has a complicated collision polygon, but still expensive even if it is a 4-point box), creating many objects in 1 tick, calling functions many times (especially if the function has an objects UID sent across and there are many instances of that object - Perhaps the new Custom Actions helps with lowering the need for C3 to check every object to find the correct UID).
If something IS expensive but required, and is running every tick, you can try to think "OK, when do I actually need these events to run?". In some cases, it is required to run some events every tick, but sometimes you only need to run events occasionally, E.G. Only when a player's position changes - In this case, you could track the LastX and LastY of the player, and then every tick, check if self.LastX is NOT equal to self.X (same with LastY in an "Or" block), and then throw your events into here as sub-events, then at the very end of these sub-events, set LastX and LastY to self.X and self.Y (Note: This has the added benefit of allowing you to check distances travelled in pixels since the last tick). So overall, this creates another check every tick where it checks the players position compared to LastX and LastY, but this could significantly save on performance if the sub-events within this are quite expensive. I may not have chosen the best example as you may only have 1 or 2 player instances that are usually always moving around, making this slightly more expensive overall, but the same concept could be applied to anything that has multiple instances existing and each instance could be either moving or stationary, such as many interactive items scattered around the map and such.
With collisions, I find this can vary significantly - Sometimes it is best to filter the object picking first, and try to keep the collision event at the very end, so that less collision checks are occurring. But, sometimes utitilising the collision cells in C3 works well, so it can sometimes be more performant to keep collisions at the very top of an event block - Definitely worth experimenting and measuring CPU performance each time to see what is best for your situation.
With effects, particularly 1-frame objects with a non-animated effect (such as AdjustHSL on a scenery object) if you do not often need to adjust the effect parameters, then consider using the drawing canvas - You can paste the object with its effects, then hide/disable the original object's effects, which helps with GPU performance. If you must use a sprite for this object, you could implement a system to load the drawing canvas result back into the sprite via "Load image from URL", which if you make this system, you could do this for each animation frame. Again though, this is only for non-animated effects such as AdjustHSL, and not for effects such as warping water or noise effects.
With effects on objects or layers, try to disable them when not needed, even if you have AdjustHSL or Contrast and they are at their default values that show no visual difference - At the end of the day, it is still generating the effect, so disabling and enabling effects when needed is key.
Similar to the above, but for behaviours - If objects are offscreen or invisible, their behaviours simply don't need to be active when the player cannot see them or their behaviour is not important to anything else that the player can see, although be careful and always test thoroughly (E.G. You may not want physics disabled when offscreen, as this could cause issues with other physics objects colliding with it, if any. Thankfully, physics objects "sleep" when they don't move for a while, but I perceive this as "they are asleep, but they're actively awaiting an unexpected object to smash into them, so it must be doing some form of collision check even when sleeping").
If you must spawn multiple objects quickly, try to spread this out across multiple ticks if it won't cause issues for what you're trying to achieve. If you need to spawn objects onscreen that the player will instantly see, then it is not recommended to do this as this could be visually jarring, but sometimes it would not have any visual impact at all (E.G. If you had 10 objects explode and 3 particle objects are spawned for a fire & smoke effect, you could wait a tick for each of the 3 particle effects to spawn in, so that you only have 10 objects spawning in 1 tick, rather than 30 spawning in 1 tick - a fire effect sort of gently appears and fades in, so this would not look visually jarring if the smoke particle spawns in a tick later). Again, this is very situational, but always worth considering. Anything for dem sweet performance gains.
Further to the above, you could create the objects earlier and have a "pool" of objects to pick from, so that you are not creating many objects per tick. If you can determine a minimum amount of objects that would ever need to appear at once, and it's not over like 50 objects (50 is a random number, could be lower on mobile), then you could create these at start of layout and keep them invisible, make sure everything about the object is disabled (no behaviours enabled, particles not spraying), and have a Boolean value in the object called "IsActive", then upon needing one of these objects, you can simply do an event for "IsActive = False" and "pick nth instance 0" to get an object, and then set IsActive to true whilst it is in use (and set it back to false and hide it again when no longer needed - don't "hide" the object every tick, do it in the event where the object is becoming inactive).
With particles, if you are a heavy user of particles, consider dynamically adjusting the rate/lifetime whenever many particles exist (rate is probably more appropriate, since lifetime could visually ruin an effect, but still worth considering). E.G. If you had to spawn in 10 confetti particles effects, then upon creating these, check how many other active particles exist and lower the particle rate, and also pick other existing particles to lower their rate too. It is best to determine the lowest rate that looks visually appealing, so that you don't set the rate too low and ruin the effect. If there's many particle objects, it may be expensive to have to loop through all objects to lower their rate, so be careful and mindful about this, and always measure performance.
Try to not have too many layers with "force own texture". I always thought many types of effects required "force own texture", but it's surprisingly few and far between - In my effect-heavy game, out of about 30 layers, I only need 2 layers with "Force own texture" enabled. It is mostly needed when using blend modes such as "destination out", "source in", etc, and whenever you want to blend two additive objects without taking into account the layers below. If you require blend modes such as destination/source, then it may be worth exploring the drawing canvas plugin, as you can paste objects with blends into the drawing canvas and it can "carve" shapes out of the drawing canvas and such. Pasting objects can be more expensive overall, but if its between using "force own texture" on a layer, compared to pasting 2 objects on a 100x100 drawing canvas, then it might be worth going with the drawing canvas (again though, measure CPU/GPU performance to see if it was truly worth it!)
When using the Preview Debugger to watch event CPU usage, you may find that a specific group is reaching high CPU usage. If you are unsure what could be causing it, you can create many small groups in this group, name them A, B, C, D, etc., and then put only 1 event in each of these groups (don't accidently reorder your events!). Then upon checking the debugger again, you are likely to discover which specific event was causing the high CPU usage, and you can figure out a solution from here.
It is worth having a SpriteFont always onscreen with a few variables displayed, such as FPS, CPU utilisation, GPU utilisation, and Object Count. Recently, I found that my large project was lagging after long play sessions, but I realised the "object count" was increasing way higher than it should, which gave me the clue that I was not destroying objects correctly - After realising this, I could then use the debugger, play the game for a while, check the list of all object counts in the debugger, and discovered which specific object was not getting destroyed and resolved the lag issue.
That's a few thoughts off the top of my mind, hope someone finds any of this useful. I'm interested to learn more and am curious what anyone else will post.