preface

Recently, the company’s project needs to optimize the H5 page in seconds, so we investigated the plan on the market, and combined with the company’s specific business needs to do an optimization practice in this aspect. This article is a record of this optimization practice, and the source code is attached to download at the end of the article.

Look at the effect

Optimization idea

First, let’s look at the steps to load an H5 page on iOS platform:

Initialize webView -> request page -> Download data -> parse HTML -> request JS/CSS resources -> DOM render -> parse JS execution -> JS request data -> parse render -> download render image

Since all the pages seen by users before DOM rendering are white screens, the optimization idea is to analyze the time spent in each step before DOM rendering and optimize the part with the highest cost performance. This can be divided into the front-end can do optimization, and the client can do optimization, the front-end needs to cooperate with the front-end side, not discussed in this article, this article mainly discusses the client can do optimization ideas. The general idea goes something like this:

  1. Can cache as far as possible cache, with space for time. Here can go to intercept all resource requests of the H5 page, including HTML, CSS/JS, pictures, data, etc., the right client to take over the cache strategy of resources (including the maximum space occupied by the cache, the elimination of the cache algorithm, cache expiration strategy);
  2. What can be preloaded, preloaded in advance. It can deal with some time-consuming operations in advance, such as initialization of webView waiting to be used in advance when the App starts;
  3. Where it can be done in parallel, it will be done in parallel, taking advantage of the multi-core capabilities of the equipment. For example, when loading webView, you can load the resources needed at the same time;

Initialize the WebView phase

Loading a web page on the client is different from loading a web page on the PC. On the PC, you directly enter a URL in the browser to start the connection. On the client, you need to start the browser kernel first, initialize some global services and resources of the WebView, and then start the connection. Take a look at how long this phase of Meituan testing took:

When the client opens the H5 page for the first time, there will be a webView initialization time,

You can see that with WKWebView, the first initialization takes more than 760 milliseconds, so if you can load the web page with the already initialized WebView, then this part of the time is not needed.

Here we implement a webView buffer pool solution, which is initialized when the App starts. When we need to open a web page, we can directly fetch the Webview from the buffer pool:

+ (void)load
{
    [WebViewReusePool swiftyLoad];
}

@objc public static func swiftyLoad(a) {
    NotificationCenter.default.addObserver(self, selector: #selector(didFinishLaunchingNotification), name: UIApplication.didFinishLaunchingNotification, object: nil)}@objc static func didFinishLaunchingNotification(a) {
    // Preinitialize the WebView
    WebViewReusePool.shared.prepareWebView()
}

func prepareWebView(a) {
    DispatchQueue.main.async {
        let webView = ReuseWebView(frame: CGRect.zero, configuration: self.defaultConfigeration)
        self.reusableWebViewSet.insert(webView)
    }
}
Copy the code

Establish a connection -> DOM pre-render stage

# request blocking

SetURLSchemeHandler method provided by WKWebView can be used to add a custom Scheme on iOS 11 and above. Compared with the Scheme of NSURLProtocol private API, there is no audit risk. Then you can intercept all custom requests in the WKURLSchemeHandler protocol:

// Custom interception request starts
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
    let headers = urlSchemeTask.request.allHTTPHeaderFields
    guard letaccept = headers? ["Accept"] else { return }
    guard letrequestUrlString = urlSchemeTask.request.url? .absoluteStringelse { return }

    if accept.count> ="text".count && accept.contains("text/html") {
        / / HTML to intercept
        print("html = \(String(describing: requestUrlString))")
        // Load local cached resources
        loadLocalFile(fileName: creatCacheKey(urlSchemeTask: urlSchemeTask), urlSchemeTask: urlSchemeTask)
    } else if (requestUrlString.isJSOrCSSFile()) {
        / / js | | the CSS file
        print("js || css = \(String(describing: requestUrlString))")
        loadLocalFile(fileName: creatCacheKey(urlSchemeTask: urlSchemeTask), urlSchemeTask: urlSchemeTask)

    } else if accept.count> ="image".count && accept.contains("image") {
        / / picture
        print("image = \(String(describing: requestUrlString))")
        guard letoriginUrlString = urlSchemeTask.request.url? .absoluteString.replacingOccurrences(of:"customscheme", with: "https") else { return }
				
      	// Images can use the cache strategy provided by SDWebImageManager
        SDWebImageManager.shared.loadImage(with: URL(string: originUrlString), options: SDWebImageOptions.retryFailed, progress: nil) { (image, data, error, type, _._) in
            if let image = image {
                guard let imageData = image.jpegData(compressionQuality: 1) else { return }
              	
                // Resend the request if the resource does not exist
                self.resendRequset(urlSchemeTask: urlSchemeTask, mineType: "image/jpeg", requestData: imageData)
            } else {
                self.loadLocalFile(fileName: self.creatCacheKey(urlSchemeTask: urlSchemeTask), urlSchemeTask: urlSchemeTask)
            }
        }

    } else {
        // other resources
        print("other resources = \(String(describing: requestUrlString))")
        guard let cacheKey = self.creatCacheKey(urlSchemeTask: urlSchemeTask) else { return }
        requestRomote(fileName: cacheKey, urlSchemeTask: urlSchemeTask)
    }
}

/// call when the custom request ends
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask){}Copy the code
Implement resource caching

Here using SWIFT to achieve memory and disk cache logic, main reference (Chao) test (XI) YYCache ideas and source code, memory cache using double linked list (logic) + hashMap (storage) LRU cache elimination algorithm, add, delete, change and check are O(1) time complexity, Disk caching uses sandbox file storage. The two kinds of cache realize the cache management of three dimensions: cache duration, cache quantity and cache size.

The way the protocol is used defines the interface API:

protocol Cacheable {
    associatedtype ObjectType
    
    /// Total number of caches
    var totalCount: UInt { get }
    /// Total cache size
    var totalCost: UInt { get }
    
    /// Whether the cache exists
    ///
    /// -parameter key: cache key
    /// - Returns: results
    func contain(forKey key: AnyHashable) -> Bool
    
    // return the cache for the specified key
    ///
    /// - Parameter key:
    /// - Returns:
    func object(forKey key: AnyHashable) -> ObjectType?
    
    // Set cache k and v
    ///
    /// - Parameters:
    /// - object:
    /// - key:
    func setObject(_ object: ObjectType, forKey key: AnyHashable)
    
    // Set cache k, v, and C
    ///
    /// - Parameters:
    /// - object:
    /// - key:
    /// - cost:
    func setObject(_ object: ObjectType, forKey key: AnyHashable, withCost cost: UInt)
    
    // delete the cache for the specified key
    ///
    /// - Parameter key:
    func removeObject(forKey key: AnyHashable)
    
    // delete all caches
    func removeAllObject(a)
    
    // clean up according to the cache size
    ///
    /// -parameter cost: cache size
    func trim(withCost cost: UInt)
    
    // clean up according to the number of caches
    ///
    /// -parameter count: indicates the number of caches
    func trim(withCount count: UInt)
    
    // Clean up according to the cache duration
    ///
    /// -parameter age: cache duration
    func trim(withAge age: TimeInterval)
}

extension Cacheable {
    func setObject(_ object: ObjectType, forKey key: AnyHashable) {
        setObject(object, forKey: key, withCost: 0)}}Copy the code
Usage:
/// h5 page resource cache
class H5ResourceCache: NSObject {
    // memory cache size: 10M
    private let kMemoryCacheCostLimit: UInt = 10 * 1024 * 1024
    /// Disk file cache size: 10M
    private let kDiskCacheCostLimit: UInt = 10 * 1024 * 1024
    /// Disk file cache duration: 30 minutes
    private let kDiskCacheAgeLimit: TimeInterval = 30 * 60
    
    private var memoryCache: MemoryCache
    private var diskCache: DiskFileCache
    
    override init() {
        memoryCache = MemoryCache.shared
        memoryCache.costLimit = kMemoryCacheCostLimit
            
        diskCache = DiskFileCache(cacheDirectoryName: "H5ResourceCache")
        diskCache.costLimit = kDiskCacheCostLimit
        diskCache.ageLimit = kDiskCacheAgeLimit
        
        super.init()}func contain(forKey key: String) -> Bool {
        return memoryCache.contain(forKey: key) || diskCache.contain(forKey: key)
    }
    
    func setData(data: Data, forKey key: String) {
        guard let dataString = String(data: data, encoding: .utf8) else { return }
        memoryCache.setObject(dataString.data(using: .utf8) as Any, forKey: key, withCost: UInt(data.count)) diskCache.setObject(dataString.data(using: .utf8)! , forKey: key, withCost:UInt(data.count))}func data(forKey key: String) -> Data? {
        if let data = memoryCache.object(forKey: key) {
            print("This is the memory cache.")
            return data as? Data
        } else {
            guard let data = diskCache.object(forKey: key) else { return nil}
            memoryCache.setObject(data, forKey: key, withCost: UInt(data.count))
            print("This is disk cache.")
            return data
        }
    }
    
    func removeData(forKey key: String) {
        memoryCache.removeObject(forKey: key)
        diskCache.removeObject(forKey: key)
    }
    
    func removeAll(a) {
        memoryCache.removeAllObject()
        diskCache.removeAllObject()
    }
}
Copy the code

The effect

Matters needing attention

#1. The crash occurs when the network load callback is still called after the WKURLSchemeHandler object instance is releasedThe task has already been stoppedThe error

Solution: Use a dictionary to hold the state of the WKURLSchemeTask instance at the beginning and end of the interception request
// MARK:- Request interception begins
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
    holdUrlSchemeTasks[urlSchemeTask.description] = true
}

/// call when the custom request ends
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
    holdUrlSchemeTasks[urlSchemeTask.description] = false
}

// Add a layer of judgment where you need to use urlSchemeTask instances
// Whether urlSchemeTask ends prematurely, after which the instance method will crash
if let isValid = self.holdUrlSchemeTasks[urlSchemeTask.description] {
    if! isValid {return}}Copy the code

#2. Garbled web pages

Add network request response receiving format:

 manager.responseSerializer.acceptableContentTypes = Set(arrayLiteral: "text/html"."application/json"."text/json"."text/javascript"."text/plain"."application/javascript"."text/css"."image/svg+xml"."application/font-woff2"."application/octet-stream")
Copy the code

# 3. WKWebView hang

/ / white
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    if webview.title == nil {
        webview.reload()
    }
}

/ / white
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
    webView.reload()
}
Copy the code

The source code

Github.com/ljchen1129/…

TODOList

  1. Writing unit tests
  2. Remove third-party libraries SDWebImage and AFNetworking and use native implementations
  3. Resource preloading logic
  4. Unified exception management
  5. More Swift style

The resources

  1. blog.cnbang.net/tech/3477/
  2. Mp.weixin.qq.com/s/0OR4HJQSD…
  3. Juejin. Cn/post / 684490…
  4. Tech.meituan.com/2017/06/09/…

If you are interested, you can follow my public account “Qingzheng Elder Brother”.