It’s less about performance but more about canonicity and how you can tie your work with what the engine can afford or what you can come up with. When a yield is required, I will always have an event set up, whether it relies on an engine event or a pure Luau signal. For asynchronous yields that I have no control over, I deploy Promises to call the function and give me the result to chain off of.
Regarding that first bit: the engine is very powerful so the time gains you have between a signal and a loop are very trivial, so it’s not always good to just talk about designing for performance. There’s other factors like making your code readable, maintainable, idiomatic, future-proofed, and more.
There is never a time I will agree where a loop is more suitable than an event. An event can replace a loop but a loop can’t replace an event, only work together with one. If there is an event to do what needs to be done, I will always recommend it more than the loop. On Roblox, engine events have the significant win over loops in readability and efficiency.
When you use an event or even some method that performs a task a certain way, your code is pretty future-proofed. A nice example is DistanceFromCharacter which calculates the distance between a given Vector3 point and your character. This turns over the logic of checking distance to the engine which may update how distance is calculated (e.g. different limb, center position or parameters for a valid check) and save you the trouble of needing to update so many different pieces of code to accommodate any new behaviour.
There’s a very good reason why we have the Wait method available on RBXScriptSignal and it’s so that you don’t have to form a connection for a “menial task” – it allows you to get whatever arguments the signal fired with right away. I don’t fancy the case of iterations repeatedly running when they don’t need to, and prefer to yield my code until events fire which then signal the scheduler to resume the thread.
My advice is that you should find those events you need, or build a system dedicated to the task that you want - don’t use loops unless you need them.
local Players = game:GetService("Players")
local LocalPlayer = Players.LocalPlayer -- Implicit to a LocalScript
-- If there's no character, return what CharacterAdded fires with next,
-- which is guaranteed to be a character model.
local Character = LocalPlayer.Character or LocalPlayer.CharacterAdded:Wait()
-- Internal equivalent is a FindFirstChild, and if nil, then ChildAdded:Wait()
-- until a match is met. Optional timeout to end this process early.
local RootPart = Character:WaitForChild("HumanoidRootPart")
This is much more readable than any loop I would ever hope to think of for a similar case on the post you were asking for input on, and maintainable. In Deferred mode you don’t even need to wait for the HumanoidRootPart because the event is arriving later in the frame. Personally, if anything, I have the exact opposite view of that post - I would hate writing a whole loop to accomplish something trivial, and would only do so if there’s no other option (obvious examples include traversing tables).
Ultimately though, Promises are surely the way to go when you want to handle coordination of multiple asynchronous events, chain off them and solve some really common issues with yields. An example of where Promises saved me was in fact the very same case as yours with yielding for data. I have a method that allows scripts to wait for data which in turn will see what happens first: the data loads (or is loaded) and gives back the data, the data unloads and errors with nothing to return or the player leaves so it errors with nothing to return.
No way I’d use a loop to handle complex logic like this. It’s all based in events and it’s not terribly unreadable either.