So I was confronted with an interesting bug this week, and I wanted to share it with everyone so maybe it will save you some time. Put simply, NSAttributedString with NSHTMLTextDocumentType is slow. Dog slow. So obscenely slow that it should probably never, ever be used.
Consider the following code, for example:
This is the “Apple-approved” way to render HTML into an attributed string (and onto the screen without using a WebView.)
Say you put this in the
viewDidLoad method of a view controller and trigger it with a segue from a UITableView. What happens?
Your UI locks dead for at least a half-second in the simulator. Run it on an old or underpowered device like an iPod Touch, and watch it completely lock for seconds. Doing this brings an app to its knees and makes it nearly completely unusable.
How I Found This
I was troubleshooting a problem with navigation. There was a noticible delay in pressing an item in a UITableView, and the corresponding segue to the detail view and I was supposed to figure out why this was happening. From a high level, everything looked okay. I couldn’t see anything that would obviously cause an issue. Anything that was could conceivably lock the UI was done in the background.
So I fired up the app in Instruments (Product -> Profile) and attached a “Time Profiler” to the app. Ran the app and tried to navigate. I immediately noticed something pegging the CPU for a second or so, a very noticible flat graph. I paused execution, selected that area, and drilled down about 20 levels. This is finally what I found:
Running Time Self Symbol Name 577.0ms 61.7% 0.0 -[NSAttributedString(NSAttributedStringUIFoundationAdditions) initWithData:options:documentAttributes:error:]
577 milliseconds spent rendering a very simple string to HTML. Commenting out the offending line resulted in nearly instantaneous navigation. I found my offender. But seriously … wat? I could understand if I was asking it to render a huge piece of HTML, but this was like a couple of sentences with some links tags. Not exactly heavy lifting.
Working Around It
So I was left with a couple of options. Render it in the background and update the UI when it’s ready is one option. May not even be able to do that depending on how it works internally (I didn’t actually try this.) Besides, it’s still slow and would result in the display “glitching” when the UI filled in. Not really ideal.
Instead I decided to use an open source library called DTCoreText. This provides some additions to NSAttributedString that allow it to handle HTML without using the built-in HTML handlers.
Using this library, the rendering time for the same string went down to about 60 milliseconds as profiled in Instruments, almost a tenfold reduction in rendering time and enough of a reduction that navigation is now, from a user’s perspective, instantaneous.
One thing of note, if you are using DTCoreText, the built in “label” tool that is included with DTCoreText doesn’t work very well. It’s directly descended from UIView and is not auto-layout aware. This is a good place to take the NSAttributedString from DTCoreText and feed it into TTTAttributedLabel.
But, with TTTAttributedLabel, you will need to clean up the NSAttributedString just a bit. Otherwise, links will not have the correct formatting. This is a known issue with DTCoreText that hasn’t been addressed yet.
So you could do something like this:
That will remove the
CTForegroundColorFromContext attribute from links. And then it will render in TTTAttributedLabel beautifully.