NSHTMLTextDocumentType is Slow

By · Published · apple, cocoa, ios

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 )

Did something I wrote 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.


Related Posts

UILocalNotifications and time zones

The 2018 MacBook Pro Sucks


comments powered by Disqus