Let me add my two cents here as well
Threading is the only warranty to avoid a freeze. Such a freeze will happen if something expects the program to react within a certain time, but it can't, because it's busy. So you need to find a trade-off between speed (Application.ProcessMessages only every nth entry, with a huge n), and reaction time on the slowest machine for the slowest entry (small n). You'll always make sacrificies.
Some kind of visual feedback is helpful (e.g. TProgressBar) - if the user in front of the program knows it's working, he's less likely to click and cause a freeze. But updating UI is slowing down massively, since Application.ProcessMessages will take longer.
Since UI updating is an issue even with threading operations, I often take a different approach: I have an external "progress" object (for threading, I use a descendant of TMultiReadExclusiveWriteSynchronizer), and the main UI has a temporary timer that simply refresh the progress every 40 to 100 ms (1/25 to 1/10 second), taking input from that object as needed. This means that only those steps that are "visually important" to the user are updated to the UI. So even with many Application.ProcessMessages, the UI updating messages are kept low.