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}
  documentAttributes:nil
               error:&err];
[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.

Comments (0)

Interested in why you can't leave comments on my blog? Read the article about why comments are uniquely terrible and need to die. If you are still interested in commenting on this article, feel free to reach out to me directly and/or share it on social media.

Contact Me
Share It
Apple
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.
Read More
What I Use
Since it’s been a good six years since I did one of these, here’s what I am using in the year 2022 as far as tech and tech-adjacent things.
Read More
What I Use
Since it’s been awhile since I wrote a post about what I use in regards to software, hardware, etc. Perhaps it’s time that I did that again. So here’s a list of what I’m using in 2016:
Read More