Don't use timers, afterRender, or dynamic component generation just to try and shave off startup time, because the end result will be longer waits until your application/component is usable. The worse offenders in Lightning are rendering cycles and multiple trips to the server, plus Locker Service, which adds additional overhead.
Lightning is already a lazy-loading framework, by definition. Your initial component graph is calculated, a single request is made to the server to load/cache all of your components, they are then initialized, and then a second rendering cycle occurs when you receive your callback from $A.enqueueAction and you set your attributes. It is true that Lightning needs more optimization, and Salesforce is working on that (e.g. Summer 17 has a number of performance optimizations). Let's look at the options you've presented and see which ones might be useful.
This is not ideal. Either not everything is loaded yet, or you have idle time before your lazy-loading kicks in. Either way, you won't get perfectly optimal experiences on all devices if you pick an arbitrary time to wait before doing stuff. It also virtually guarantees you'll have more rendering cycles than necessary.
Useless for lazy-loading. AfterRender simply tells you the DOM is ready, which is already after a rendering cycle. If you do anything here within the framework, you're going to cause another rendering cycle. Worst case scenario is an infinite loop, best case scenario is additional, unnecessary rendering cycles. AfterRender is a tool for using additional libraries to modify the DOM post-rendering.
Dynamic Component Creation
This has several challenges, the least of which is if you do it wrong, you're introducing additional round trips to the server and additional rendering cycles. Components statically referenced can be loaded during the initial load, reducing server round-trips and possibly rendering cycles. This results in better overall performance in exchange for some additional loading time. The only real benefit you can get out of this is to pre-cache components that you're not using on start-up but that you might need during execution.
This is how Lightning already works, and is intended to be used. You can load data on-demand at the moment the user clicks on a button, link, tab, etc. You should be doing this already. It is not always necessary to load all of the data upfront, and you can usually avoid some of the initial start up time by only loading data that will be initially presented to the user, and then going back to the server to pick up more data, as necessary.
This isn't even always required, as I have written code like this one that loads 1,000 accounts in a small fraction of a second; most of the time spent loading the app is literally just loading Aura/Lightning, not my code or data. Keep in mind that components you don't load upfront will cause a delay in rendering as Lightning has to query the server for components not already in its cache.
Event-driven loading is the preferred way to optimize the experience. It's probably easiest if you simply wall off areas you want to lazy-load with aura:if, then set an attribute when you want to render the area. Typical examples might include additional tabs in a tab panel, or areas hidden until a user clicks on something. You don't need to do anything special, just use aura:if.
Keep in mind that if you need to set a $A.getCallback, you're introducing additional rendering cycles, which is detrimental to performance. Dynamic component generation can be useful to load additional components, but I would avoid doing this for anything that needs to display initially. There is a reason why we have recommendations for placeholders and spinners. It gives the user a sense that something is happening, instead of just a white screen while we wait for things to load.
Part of the user experience is giving the users some sort of indication that the code hasn't crashed, and loading animations helps with that significantly. Other lazy-loading "techniques" are therefore not optimal for this purpose. The entire framework is already in the lazy-loading business, so using additional tricks really should not be necessary in most ordinary cases.
Of course, there are exceptions to the rule, so if you're having a particular challenge, you might experiment with it, but remember that "premature optimization is the root of all evil." Most of the time, there is little that can be done beyond what we're given to work with. You can expect further performance gains in Lightning as the technology matures.