NSHTMLTextDocumentType is Slow

This is an old post!

This post is over 2 years old. Solutions referenced in this article may no longer be valid. Please consider this when utilizing any information referenced here.

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:

NSString *htmlStr = @"I'm a simple string with some <i>html</i> in it.";
NSError *err = nil;
NSAttributedString *attrStr =
    [[NSAttributedString alloc]
        initWithData:[htmlStr dataUsingEncoding:NSUTF8StringEncoding]
             options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType}
[self.summaryLabel setText:attrStr];

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.

At that point, I decided to hit stack overflow. I found several reports of others encountering this same issue. Apparently, NSAttributedString/NSHTMLTextDocumentType just really, really sucks.

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:

NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithAttributedString:[[NSAttributedString alloc] initWithHTMLData:data options:realOpts documentAttributes:nil]];
[attrStr enumerateAttributesInRange:NSMakeRange(0, [attrStr length]) options:NSAttributedStringEnumerationReverse usingBlock:
 ^(NSDictionary *attributes, NSRange range, BOOL *stop) {
     if ([attributes valueForKey:@"DTGUID"] != nil) {

         // We need to remove this attribute or links are not colored right
         // @see https://github.com/Cocoanetics/DTCoreText/issues/792

         [attrStr removeAttribute:@"CTForegroundColorFromContext" range:range];

That will remove the CTForegroundColorFromContext attribute from links. And then it will render in TTTAttributedLabel beautifully.

Did this article help you out?

That's great! I don't earn any money from this site - I run no ads, sell no products and participate in no affiliate programs. I do this solely because it's fun; I enjoy writing and sharing what I learn.

All the same, if you found this article helpful and want to show your appreciation, here's my Amazon.com wishlist.

Read More

UILocalNotifications and time zones

Here’s a tip when dealing with UILocalNotifications. If you want to schedule a notification for a specific time using fireDate, you need to apply a timeZone to the UILocalNotification object. Otherwise, iOS will intepret this as an absolute, countdown-based date based on GMT.

Hierarchies: Finding Parents, Children and Descendents using Swift

It usually doesn’t take beginning macOS/iOS developers long to discover NotificationCenter and see it as the solution to every single problem of passing data around to different controllers. And NotificationCenter is great, but it has some downsides. Notably, it is very easy to introduce retain cycles (and memory leaks) unless you are very careful to track and free the listener when the object is released. This has bitten me on several occasions. In general, excessive use of NotificationCenter ends up creating a difficult to maintain app where it is not entirely clear what is responding to what and where.