cài đặt wordpress serverless bằng Cloudflare Worker

Cloudflare là một proxy CDN tuyệt vời, điều này khỏi phải bàn, hơn nữa nó còn miễn phí, nếu anh em thích dùng cloudflare mình vẫn khuyên anh em nên trả tiền cho bản pro của nó vì những tính năng ngon lành hơn, cái quan trọng nhất là “Volume of concurrent connections” của bản pro là medium, sẽ rất hiệu quả.

Cloudflare Websocket
Bạn có thể xem phần này ở tab Network

để tăng tốc độ thì như website này: manhhung.me mình sử dụng cloudflare pro và có bật argo, hiệu quả của argo khá ổn, giảm được ttfb (time to first byte) khá rõ rệt, nếu anh em nào sử dụng gói business với railgun còn tốt hơn nữa.

Cloudflare Argo WordPress
đây là hiệu quả của cloudflare argo với trang wordpress của mình, cao hơn những gì cloudflare quảng cáo, mình sử dụng cụm server singapore của google và traffic chủ yếu từ Việt Nam.

Vì sao phải sử dụng cloudflare workers? nó là gì?

để dễ hiểu thì đây giống như việc load html chứ không phải qua xử lý php nữa, việc này sẽ giảm tải được server rất nhiều, và nó được xử lý, thực thi bởi cloudflare, nếu bạn dùng blog hay trang tin tức thì còn gì bằng nữa, tốc độ phải nói là nhanh nhất hiện nay rồi, ở thời điểm hiện tại, mình vẫn chưa thấy giải pháp nào nhanh hơn, chưa kể sắp tới cloudflare còn mở loại kết nối mới QUIC (http3) ra rộng rãi, khi đó mọi thứ sẽ không còn chậm chạp như ngày hôm nay nữa, bây giờ cloudflare cho đăng ký chờ, anh em nào hứng thú thì vào đây xem và đăng ký.

Hiện tại cloudflare đang miễn phí 100,000 requests mỗi ngày cho mỗi tài khoản, hơi ít nhưng phí cũng không quá cao đâu, và bài viết này chỉ áp dụng cho các web tĩnh, woocommerce thì cao cấp hơn, phải bybass cookies và dĩ nhiên là phải sử dụng tài khoản business của cloudflare, bài viết này mình hướng dẫn cho anh em sử dụng bản free và pro có thể trải nghiệm được tính năng workers là chính.

Cài đặt CLoudflare Workers

Setup script

đầu tiên bạn đăng nhập vào cloudflare và vào phần quản lý domain của mình, chọn tab workers. bấm vào “launch editor” tạo 1 script mới trong mục “Scripts” và đặt tên cho nó, ở đây mình đặt là “serverless-cache”, sau đó bấm vào nút “edit”.

Cloudflare Workers Cho WordPress

Copy hết đoạn code trong khung dưới đây của mình vào, và sửa 3 phần email, key, zone của bạn nhé.

  • email: “”, // lấy ở https://dash.cloudflare.com/profile
  • key: “”, // Global API Key lấy ở đây https://dash.cloudflare.com/profile
  • zone: “” // “Zone ID” lấy ở mục “API section” ở dashboard page https://dash.cloudflare.com/
// IMPORTANT: Either A Key/Value Namespace must be bound to this worker script
// using the variable name EDGE_CACHE. or the API parameters below should be
// configured. KV is recommended if possible since it can purge just the HTML
// instead of the full cache.

// API settings if KV isn't being used
const CLOUDFLARE_API = {
  email: "hung.tran@xxx", // From https://dash.cloudflare.com/profile
  key: "88b421ac4xxx",   // Global API Key from https://dash.cloudflare.com/profile
  zone: "2ba3cd089xxx"   // "Zone ID" from the API section of the dashboard overview page https://dash.cloudflare.com/
};

// Default cookie prefixes for bypass
const DEFAULT_BYPASS_COOKIES = [
  "wp-",
  "wordpress",
  "comment_",
  "woocommerce_"
];

/**
 * Main worker entry point. 
 */
addEventListener("fetch", event => {
  const request = event.request;
  let upstreamCache = request.headers.get('x-HTML-Edge-Cache');

  // Only process requests if KV store is set up and there is no
  // HTML edge cache in front of this worker (only the outermost cache
  // should handle HTML caching in case there are varying levels of support).
  let configured = false;
  if (typeof EDGE_CACHE !== 'undefined') {
    configured = true;
  } else if (CLOUDFLARE_API.email.length && CLOUDFLARE_API.key.length && CLOUDFLARE_API.zone.length) {
    configured = true;
  }

  // Bypass processing of image requests (for everything except Firefox which doesn't use image/*)
  const accept = request.headers.get('Accept');
  let isImage = false;
  if (accept && (accept.indexOf('image/*') !== -1)) {
    isImage = true;
  }

  if (configured && !isImage && upstreamCache === null) {
    event.passThroughOnException();
    event.respondWith(processRequest(request, event));
  }
});

/**
 * Process every request coming through to add the edge-cache header,
 * watch for purge responses and possibly cache HTML GET requests.
 * 
 * @param {Request} originalRequest - Original request
 * @param {Event} event - Original event (for additional async waiting)
 */
async function processRequest(originalRequest, event) {
  let cfCacheStatus = null;
  const accept = originalRequest.headers.get('Accept');
  const isHTML = (accept && accept.indexOf('text/html') >= 0);
  let {response, cacheVer, status, bypassCache} = await getCachedResponse(originalRequest);

  if (response === null) {
    // Clone the request, add the edge-cache header and send it through.
    let request = new Request(originalRequest);
    request.headers.set('x-HTML-Edge-Cache', 'supports=cache|purgeall|bypass-cookies');
    response = await fetch(request);

    if (response) {
      const options = getResponseOptions(response);
      if (options && options.purge) {
        await purgeCache(cacheVer, event);
        status += ', Purged';
      }
      bypassCache = bypassCache || shouldBypassEdgeCache(request, response);
      if ((!options || options.cache) && isHTML &&
          originalRequest.method === 'GET' && response.status === 200 &&
          !bypassCache) {
        status += await cacheResponse(cacheVer, originalRequest, response, event);
      }
    }
  } else {
    // If the origin didn't send the control header we will send the cached response but update
    // the cached copy asynchronously (stale-while-revalidate). This commonly happens with
    // a server-side disk cache that serves the HTML directly from disk.
    cfCacheStatus = 'HIT';
    if (originalRequest.method === 'GET' && response.status === 200 && isHTML) {
      bypassCache = bypassCache || shouldBypassEdgeCache(originalRequest, response);
      if (!bypassCache) {
        const options = getResponseOptions(response);
        if (!options) {
          status += ', Refreshed';
          event.waitUntil(updateCache(originalRequest, cacheVer, event));
        }
      }
    }
  }

  if (response && status !== null && originalRequest.method === 'GET' && response.status === 200 && isHTML) {
    response = new Response(response.body, response);
    response.headers.set('x-HTML-Edge-Cache-Status', status);
    if (cacheVer !== null) {
      response.headers.set('x-HTML-Edge-Cache-Version', cacheVer.toString());
    }
    if (cfCacheStatus) {
      response.headers.set('CF-Cache-Status', cfCacheStatus);
    }
  }

  return response;
}

/**
 * Determine if the cache should be bypassed for the given request/response pair.
 * Specifically, if the request includes a cookie that the response flags for bypass.
 * Can be used on cache lookups to determine if the request needs to go to the origin and
 * origin responses to determine if they should be written to cache.
 * @param {Request} request - Request
 * @param {Response} response - Response
 * @returns {bool} true if the cache should be bypassed
 */
function shouldBypassEdgeCache(request, response) {
  let bypassCache = false;

  if (request && response) {
    const options = getResponseOptions(response);
    const cookieHeader = request.headers.get('cookie');
    let bypassCookies = DEFAULT_BYPASS_COOKIES;
    if (options) {
      bypassCookies = options.bypassCookies;
    }
    if (cookieHeader && cookieHeader.length && bypassCookies.length) {
      const cookies = cookieHeader.split(';');
      for (let cookie of cookies) {
        // See if the cookie starts with any of the logged-in user prefixes
        for (let prefix of bypassCookies) {
          if (cookie.trim().startsWith(prefix)) {
            bypassCache = true;
            break;
          }
        }
        if (bypassCache) {
          break;
        }
      }
    }
  }

  return bypassCache;
}

const CACHE_HEADERS = ['Cache-Control', 'Expires', 'Pragma'];

/**
 * Check for cached HTML GET requests.
 * 
 * @param {Request} request - Original request
 */
async function getCachedResponse(request) {
  let response = null;
  let cacheVer = null;
  let bypassCache = false;
  let status = 'Miss';

  // Only check for HTML GET requests (saves on reading from KV unnecessarily)
  // and not when there are cache-control headers on the request (refresh)
  const accept = request.headers.get('Accept');
  const cacheControl = request.headers.get('Cache-Control');
  let noCache = false;
  if (cacheControl && cacheControl.indexOf('no-cache') !== -1) {
    noCache = true;
    status = 'Bypass for Reload';
  }
  if (!noCache && request.method === 'GET' && accept && accept.indexOf('text/html') >= 0) {
    // Build the versioned URL for checking the cache
    cacheVer = await GetCurrentCacheVersion(cacheVer);
    const cacheKeyRequest = GenerateCacheRequest(request, cacheVer);

    // See if there is a request match in the cache
    try {
      let cache = caches.default;
      let cachedResponse = await cache.match(cacheKeyRequest);
      if (cachedResponse) {
        // Copy Response object so that we can edit headers.
        cachedResponse = new Response(cachedResponse.body, cachedResponse);

        // Check to see if the response needs to be bypassed because of a cookie
        bypassCache = shouldBypassEdgeCache(request, cachedResponse);
      
        // Copy the original cache headers back and clean up any control headers
        if (bypassCache) {
          status = 'Bypass Cookie';
        } else {
          status = 'Hit';
          cachedResponse.headers.delete('Cache-Control');
          cachedResponse.headers.delete('x-HTML-Edge-Cache-Status');
          for (header of CACHE_HEADERS) {
            let value = cachedResponse.headers.get('x-HTML-Edge-Cache-Header-' + header);
            if (value) {
              cachedResponse.headers.delete('x-HTML-Edge-Cache-Header-' + header);
              cachedResponse.headers.set(header, value);
            }
          }
          response = cachedResponse;
        }
      } else {
        status = 'Miss';
      }
    } catch (err) {
      // Send the exception back in the response header for debugging
      status = "Cache Read Exception: " + err.message;
    }
  }

  return {response, cacheVer, status, bypassCache};
}

/**
 * Asynchronously purge the HTML cache.
 * @param {Int} cacheVer - Current cache version (if retrieved)
 * @param {Event} event - Original event
 */
async function purgeCache(cacheVer, event) {
  if (typeof EDGE_CACHE !== 'undefined') {
    // Purge the KV cache by bumping the version number
    cacheVer = await GetCurrentCacheVersion(cacheVer);
    cacheVer++;
    event.waitUntil(EDGE_CACHE.put('html_cache_version', cacheVer.toString()));
  } else {
    // Purge everything using the API
    const url = "https://api.cloudflare.com/client/v4/zones/" + CLOUDFLARE_API.zone + "/purge_cache";
    event.waitUntil(fetch(url,{
      method: 'POST',
      headers: {'X-Auth-Email': CLOUDFLARE_API.email,
                'X-Auth-Key': CLOUDFLARE_API.key,
                'Content-Type': 'application/json'},
      body: JSON.stringify({purge_everything: true})
    }));
  }
}

/**
 * Update the cached copy of the given page
 * @param {Request} originalRequest - Original Request
 * @param {String} cacheVer - Cache Version
 * @param {EVent} event - Original event
 */
async function updateCache(originalRequest, cacheVer, event) {
  // Clone the request, add the edge-cache header and send it through.
  let request = new Request(originalRequest);
  request.headers.set('x-HTML-Edge-Cache', 'supports=cache|purgeall|bypass-cookies');
  response = await fetch(request);

  if (response) {
    status = ': Fetched';
    const options = getResponseOptions(response);
    if (options && options.purge) {
      await purgeCache(cacheVer, event);
    }
    let bypassCache = shouldBypassEdgeCache(request, response);
    if ((!options || options.cache) && !bypassCache) {
      await cacheResponse(cacheVer, originalRequest, response, event);
    }
  }
}

/**
 * Cache the returned content (but only if it was a successful GET request)
 * 
 * @param {Int} cacheVer - Current cache version (if already retrieved)
 * @param {Request} request - Original Request
 * @param {Response} originalResponse - Response to (maybe) cache
 * @param {Event} event - Original event
 * @returns {bool} true if the response was cached
 */
async function cacheResponse(cacheVer, request, originalResponse, event) {
  let status = "";
  const accept = request.headers.get('Accept');
  if (request.method === 'GET' && originalResponse.status === 200 && accept && accept.indexOf('text/html') >= 0) {
    cacheVer = await GetCurrentCacheVersion(cacheVer);
    const cacheKeyRequest = GenerateCacheRequest(request, cacheVer);

    try {
      // Move the cache headers out of the way so the response can actually be cached.
      // First clone the response so there is a parallel body stream and then
      // create a new response object based on the clone that we can edit.
      let cache = caches.default;
      let clonedResponse = originalResponse.clone();
      let response = new Response(clonedResponse.body, clonedResponse);
      for (header of CACHE_HEADERS) {
        let value = response.headers.get(header);
        if (value) {
          response.headers.delete(header);
          response.headers.set('x-HTML-Edge-Cache-Header-' + header, value);
        }
      }
      response.headers.delete('Set-Cookie');
      response.headers.set('Cache-Control', 'public; max-age=315360000');
      event.waitUntil(cache.put(cacheKeyRequest, response));
      status = ", Cached";
    } catch (err) {
      // status = ", Cache Write Exception: " + err.message;
    }
  }
  return status;
}

/******************************************************************************
 * Utility Functions
 *****************************************************************************/

/**
 * Parse the commands from the x-HTML-Edge-Cache response header.
 * @param {Response} response - HTTP response from the origin.
 * @returns {*} Parsed commands
 */
function getResponseOptions(response) {
  let options = null;
  let header = response.headers.get('x-HTML-Edge-Cache');
  if (header) {
    options = {
      purge: false,
      cache: false,
      bypassCookies: []
    };
    let commands = header.split(',');
    for (let command of commands) {
      if (command.trim() === 'purgeall') {
        options.purge = true;
      } else if (command.trim() === 'cache') {
        options.cache = true;
      } else if (command.trim().startsWith('bypass-cookies')) {
        let separator = command.indexOf('=');
        if (separator >= 0) {
          let cookies = command.substr(separator + 1).split('|');
          for (let cookie of cookies) {
            cookie = cookie.trim();
            if (cookie.length) {
              options.bypassCookies.push(cookie);
            }
          }
        }
      }
    }
  }

  return options;
}

/**
 * Retrieve the current cache version from KV
 * @param {Int} cacheVer - Current cache version value if set.
 * @returns {Int} The current cache version.
 */
async function GetCurrentCacheVersion(cacheVer) {
  if (cacheVer === null) {
    if (typeof EDGE_CACHE !== 'undefined') {
      cacheVer = await EDGE_CACHE.get('html_cache_version');
      if (cacheVer === null) {
        // Uninitialized - first time through, initialize KV with a value
        // Blocking but should only happen immediately after worker activation.
        cacheVer = 0;
        await EDGE_CACHE.put('html_cache_version', cacheVer.toString());
      } else {
        cacheVer = parseInt(cacheVer);
      }
    } else {
      cacheVer = -1;
    }
  }
  return cacheVer;
}

/**
 * Generate the versioned Request object to use for cache operations.
 * @param {Request} request - Base request
 * @param {Int} cacheVer - Current Cache version (must be set)
 * @returns {Request} Versioned request object
 */
function GenerateCacheRequest(request, cacheVer) {
  let cacheUrl = request.url;
  if (cacheUrl.indexOf('?') >= 0) {
    cacheUrl += '&';
  } else {
    cacheUrl += '?';
  }
  cacheUrl += 'cf_edge_cache_ver=' + cacheVer;
  return new Request(cacheUrl);
}

Sau đó bấm save lại là xong, qua bước kế tiếp, chọn tab “Routes” , “add route” sau đó nhập đường dẫn website bạn muốn áp dụng, mình chọn “/*” là mình muốn áp dụng cho toàn bộ website của mình, chọn script rồi save lại.

Cloudflare Workers Cho WordPress 1
bên khung phải chọn scrip “serverless-cache” đã tạo ở bước trên.

Cài đặt KV namespace

cài cái này giúp khi site có thay đổi nó sẽ clear cache, cái này tự động, khỏe lắm, cài đặt cũng rất dễ, vào Workers tab rồi cuộn xuống cuối trang, tạo 1 cái namespace mới, đặt tên gì cũng được, không quan trọng.

Sau đó vào phần quản lý script lại, qua tab resource, mục namespace chọn “add blinding”.

Trong khung add blinding, phần variable name, nhập vào” EDGE_CACHE” (viết hoa), không đúng là script sẽ chạy sai và không purge cache được, namespace thì chọn cái ở lúc trên mới tạo, save lại hết. Qua tab “script” chọn Deploy.

Vậy là xong bước setup ở CLoudflare, giờ tới phần wordpress của bạn.

Cài đặt trên wordpress

Cái này thì cực kỳ đơn giản, đã có 1 plugin tuyệt vời của Patrick Meenan, anh này trong team cloudflare luôn, cài là chạy thôi, không có gì đặc biệt, bạn bấm vào link này để tải plugin “cloudflare page cache” về, active nó lên và không cần cài đặt gì cả

Xong xuôi bạn vào trang Preview của mục “workers” check lại lần nữa, sau đó vào lại tab “workers” để xem analytics, nếu có chạy là ổn.

Cloudflare Workers Cho WordPress 2
Sau khi cài đặt, mấy ngày sau cloudflare mới cache hết file tĩnh được, nên phần uncache request vẫn còn nhiều, không phải bận tâm.

Vậy là xong, đây là vài bước rất đơn giản để bạn có thể sử dụng workers của cloudflare cho wordpress của mình, và bạn nên test lại tốc độ tải trang, hiệu quả hơn hẳn nhé.

Bài viết mình tham khảo từ Workers example của tác giả plugin trên github, ngoài ra còn mấy mục nữa, anh em tham khảo rồi test qua, rất hay.

Giam Ttfb Cho WordPress
đây là ttfb của web mình sau khi áp dụng cloudflare workers, thực sự tuyệt vời.