Preface
Part 7 of the CF series tutorials, the time to write the CF worker tutorial is finally ripe.
Why do I say that? The reason for leaving CF worker for later is because ”In order for a certain type of traffic to eventually be accepted and processed by the corresponding worker, the prerequisite is that the traffic must not be hit and processed by the rules of the node feature modules located before the worker in the CF traffic sequence. Different types of worker-implemented features may require coordination with rules of different node feature modules: for example, for a worker optimizing HTTP access to properly process access requests, the access requests must not be hit by the caching rules of Cache Rules located before the worker in the traffic sequence; otherwise, Cache Rules will directly respond to the access requests using content in the CDN, resulting in the consequence that the access requests never reach the worker at all, and naturally cannot be optimized by the worker. Meanwhile, the access requests also cannot be hit and processed by other node feature modules, for example, they cannot be hit by rules in WAF rules whose action is block.”
In the description of the green text section above, if I don't clarify in advance concepts such as what ”traffic sequence” is, what ”node feature modules” are, what ”Cache Rules” are, what cache rule hits are, what ”WAF” is, and what ”rules in WAF rules whose action is block” are, most people would probably be completely confused just reading this description. However, this paragraph happens to be the basic prerequisite for any worker to function properly, which cannot be avoided, let alone the many concepts such as edge networks that need to be involved later in the article.
To avoid having too many concepts in a single article that would make the logic look disorganized, and since writing a worker tutorial requires so much prerequisite knowledge, I simply arranged the worker tutorial to be written later. Before this, I started with a series of tutorials covering CF's overall solution and basic concepts, gradually sorting out the important node features located before the worker in the traffic sequence. By now, the prerequisite knowledge is basically cleared up, and I can finally start writing the worker tutorial.
Note 1: For other prerequisite knowledge, please refer to parts ”1” to ”6” of the CF series tutorials, especially ”2” (Home Data Center Series: Cloudflare Tutorial (Part 2) - Brief Introduction to the Functions of Technical Nodes in CF's Overall Solution Traffic Sequence) and ”6” (Home Data Center Series Cloudflare Tutorial (6) CF Cache Rules Feature Introduction and Detailed Configuration Tutorial)。
Note 2: In order to sort out some related concepts (some of which are just my sudden whims), this article will involve a large amount of professional knowledge spanning multiple fields. Friends who are not interested can skip directly to the practical operation section later (I didn't expect it to involve so much either, but once I thought of it, I just wanted to think it through, and then simply record it, and before I knew it, I wrote more and more~).
Note 3: This article is now only used as a demonstration tutorial for CF worker configuration, and is no longer recommended from the perspective of achieving ”WordPress cache acceleration” (as CF official technical support is no longer provided). For friends with needs in this area, please refer to my other article:Home Data Center Series - Let Cloudflare and WordPress Cache Work Together: Super Page Cache Plugin Practical Guide)
However, for those who are truly interested in technology, you might as well read it, and I believe you will gain some insights.
Introduction to CF worker related concepts
What is CF worker?
Simply put, CF worker is a serverless way provided by CF for users to run JavaScript code (it can be simply understood as CF using the CPU and memory of its own edge network servers to help users run JS code and return the results to users). In addition to running JS code normally to implement the features written in the code itself (such as some unconventional forward proxies and reverse proxies, though these are not covered in this article), the most critical part is that through worker, some CF features can be controlled, such as edge cache. Compared with the original Free plan's rough control of CDN through Cache Rules (or Page Rules) (which only has two options: bypass cache and eligible for cache, at most with edge TTL and browser TTL time control options), this method of controlling edge cache through worker can provide more and finer-grained cache control, thus offering faster website access speeds in comparison.
Note 1: In fact, even though the original Free plan could only roughly control CDN through Cache Rules (or Page Rules) and did not support advanced targeted services or personalized features based on Cookies, CF's basic features can still route user requests to the nearest available data center through Anycast IP technology and its global network. It's just that due to well-known reasons, domestic visitors cannot enjoy this (foreign visitors have never complained about negative optimization~), so this has nothing to do with CF.
Note 2: It is true that using worker to control edge cache can improve website access speed, but the implementation principle is completely different from the commonly mentioned Preferred IP (I will talk about Preferred IP related knowledge separately in the next section). As for the acceleration principle, it is actually the same as another paid feature of CF: APO for WordPress, which is why I call it the ”poor man's APO for WordPress” feature.
Note 3: Since worker is inherently built on edge network servers, it naturally possesses advantages. For example, it can run JS code on the CF edge network close to users to optimize and customize HTTP requests and responses, improving application performance, reducing latency, and enhancing security, ultimately achieving the goal of improving the user access experience.
Note 4: In addition to JavaScript, worker also supports Rust or other WebAssembly languages.
Note 5: For the Free plan, the free quota for worker is 100,000 requests per day.
Additional knowledge: Preferred IP
Clarification on the concept of ”Preferred IP”
First of all, it needs to be clarified that the term "Preferred IP" is not an official name from CF, but an informal term used by some users and third-party service providers to describe CF's China Network(China Network) feature. It is very likely because this feature improves access speed in mainland China through optimized IP paths (mainly using the networks and data centers of domestic cloud partners), so users are accustomed to calling it ”Preferred IP”. This expression does not appear in CF's official documentation or product pages, so it can only be considered an informal, unofficial term.
The ”China Network” feature cannot be provided separately; it requires an Enterprise plan to obtain, and the price can only be ”customized”:

The corresponding official feature of this feature abroad isArgo Smart Routing, but it is still expensive:

Additionally: In principle, website access acceleration achieved in China through the preferred IP (Mainland China access) feature and website acceleration achieved through worker-controlled edge caching in this article are completely unrelated in their underlying logic (preferred IP has broader applicability and is more advanced, while the method described in this article is more restrictive and only suitable for HTML-type applications). However, given the habit of some friends who like to associate website acceleration with the preferred IP feature whenever it is mentioned (for example, the saying "achieving preferred IP feature through worker"), I think it is better to explain the preferred IP feature thoroughly first.
Note: For the sake of everyone's reading habits, I will still keep the colloquial term "preferred IP" later, but everyone must remember its true meaning.
How Preferred IP (China Network Access) works
The CF Enterprise plan's preferred IP feature in China has always made the vast number of friends who are used to freeloading drool. Unfortunately, Free plan users can only watch from afar but cannot touch. However, although they covet the preferred IP feature, I believe the vast majority of friends only think about how to freeload, without paying much attention to what the underlying technical principles of the preferred IP feature actually are (doesn't this sound a bit like describing a scumbag?).
CF's preferred IP feature improves access speed and reduces latency by optimizing CF's internal network routing and selecting the best path and data center to handle user requests. This feature is transparent to users and does not change the IP address obtained when users resolve domain names, but it can significantly improve website performance and stability. It is based on the following criteria:
- Intelligent Routing: Dynamically evaluates network conditions, including latency, bandwidth, and congestion, to select the fastest path for data transmission.
- Load balancing: Distributes requests to the best-performing servers based on server load to avoid single-point overload.
- Geographic Location: Routes requests to the nearest data center based on the user's geographic location to reduce latency.
- Real-time Monitoring: Continuously monitors network and server status, automatically adjusting routing strategies to cope with network changes and emergencies.
The above description is quite official and not easy to understand, so let me break it down again from the user's perspective.
- IP Address Resolved by the User for the Domain Name:
• When a user resolves a domain name using CF through DNS, they will get an IP address assigned by CF, usually CF's Anycast IP address (Anycast IP technology appeared in 1989 and began to be widely popularized after 1993. The Local DNS addresses of domestic operators in China are basically based on Anycast IP technology, but they are generally only valid within a certain regional network, or at most nationwide. What makes CF awesome is that it is valid globally). This IP address is usually universally applicable worldwide and has no direct connection with the ”preferred IP” feature. For example: a domain name that supports the preferred IP feature and a domain name that does not support it can resolve to the exact same IP address. In other words, the ”preferred IP” feature actually has absolutely nothing to do with the resolved IP address. To describe it accurately, ”preferred routing” or ”routing optimization” would be more appropriate (which is why the official CF feature name ”Argo Smart Routing“ is the correct one).
Note 1: Anycast IP technology requires the BGP dynamic routing protocol as support. Therefore, if you have a large network that you can fully control (and have an independent ASN, and public IPv4 or IPv6 address blocks) while being connected to the external network via EBGP, as long as you properly configure routing within your controlled network (internal dynamic routing protocol + IBGP), you can make some of your public IPs appear as Anycast IPs to the external network.
Note 2: And if at this time you cooperate with specific tunnel protocols (such as MPLS) in your own network to create the best path between multiple egresses/ingresses (suppose your large network has 2 ingress/egress points, normal packets enter your network from ingress/egress 1, and then exit from ingress/egress 2 according to normal routing, which takes 20 hops. But if there is a tunnel based on technology like MPLS connecting ingress/egress 1 and 2, and you can use a special method, such as a dedicated line, so that packets coming out of ingress/egress 1 can go directly to ingress/egress 2, then the network nodes passed through this way would only be 2 hops), and then make it effective for the subsequent packet sequence corresponding to accessing the target domain name ”blog.tangwudi.com”, then when the request to access ”blog.tangwudi.com” enters from entry point 1, it will go directly through this tunnel to ingress/egress 2 and then exit. How many hops does this optimization reduce? And what if the network you can control is spread all over the world (like Cloudflare)? This is just from the perspective of routing optimization. What if we also add data centers all over the world that can pull from the origin nearby? Therefore, it is completely understandable why the preferred IP feature is expensive.
Note 3: Theoretically, drawing a diagram here would make it easier to understand, but it's too tiring, so never mind...
- CF Internal Routing Optimization:
• The preferred IP feature is mainly to optimize CF's internal routing, making request transmission more efficient within CF's global backbone network (which is the China network domestically). It accelerates request processing, reduces latency, and improves overall performance by selecting the best network path and data center.
• This optimization is transparent, meaning that the IP address obtained by the user resolving the domain name remains unchanged; what changes is only how CF internally processes and transmits these requests. For example, if it is a website of a domestic CF Enterprise plan user, although the IP resolved from its domain name is the same as that of other Free plan users' websites, visitors to the Enterprise user's website can use this IP to directly enter the CF edge network from the data centers of CF's domestic partner (currently JD Cloud):

These so-called 30 data centers located in mainland China are the data centers of its current domestic partner (JD Cloud):

On the other hand, visitors of Free plan users can only obediently enter the CF edge network from the US West data centers. This gap is by no means small.
- CF's Preferred IP feature in China:
• In regions where CF does not directly control the network (such as mainland China; abroad, CF basically builds its own backbone networks directly extending to various countries and cities, and connects directly with various ISPs), the Preferred IP feature may be subject to certain limitations, especially when the partner's network infrastructure or routing policies cannot fully support CF's optimization needs.
• Due to these dependencies, the effectiveness of the Preferred IP provided by CF in these regions is likely not as significant as in regions with its own controlled global backbone network (though it is still much faster than accessing Free plan users' websites, which requires routing through US data centers).
Note: In fact, in addition to the routing optimization mentioned above, the Preferred IP feature also needs to cooperate with many key technical components, such as ”protocol-layer optimization”, ”TLS optimization”, ”intelligent DNS resolution”, ”application-layer routing”, ”real-time performance monitoring”, etc. Speaking of technical complexity, it is at least as high as a three- or four-story building.
Many friends on the Free plan use software to manually test and find the ”optimal custom IP” (which, to be precise, should be described as ”finding the IP that looks best overall in the test results at that time”), but this method has significant drawbacks:
- Manual operation is complex and time-consuming: Manual testing and selecting the best IP require a lot of time and effort, especially when frequent testing and adjustments are needed. Users must constantly monitor network conditions and perform tests to ensure that the selected IP address is always the best.
- Lack of real-time responsiveness: Network conditions change dynamically (the same IP might test best at this moment, but not be the best a few minutes later). The results of manual testing can quickly become outdated and fail to reflect the current network status in a timely manner. This means that even if a best IP is selected, it may quickly lose its advantage.
- Lack of a global perspective: Individual users' tests are usually limited to their own geographical location and network environment, failing to comprehensively reflect the best IP choices under different regions and network conditions, whereas CF's Preferred IP feature can optimize based on global network conditions.
When I first started learning to build a blog in the second half of last year, I built it using an ICP-filed domain name (I was still very naive and innocent back then, not knowing how deep the pitfalls of building a personal blog in China were; those interested in this can refer to the article:Home Data Center Series: Independent Personal Blog Setup and Pitfall Avoidance Guide), and I tried using CF Custom Hostnames + manually selecting the best IP. The feeling at that time was instability; it felt okay at the very beginning, but slowed down after a while, which forced me to constantly pay attention to changes in access speed, which was very exhausting. Later, I just used Tencent Cloud CDN directly for domestic access.
Why does using worker to control edge cache achieve better access results than CF's regular CDN?
This is mainly because Workers have the following key features:
- Flexible programming capabilities:
Workers allow developers to write custom JavaScript code to handle requests. This flexibility means you can write logic to select and route traffic to different IP addresses, thereby achieving better results.
- Edge computing advantages:
CF Workers run on globally distributed edge nodes, enabling them to quickly respond to and process user requests. Through Workers, you can dynamically select the best IP address based on real-time network conditions, geographical location, server load, etc.
- Bypassing basic feature limitations:
Although the default feature set in the Free plan may not support some advanced routing and load balancing features, Workers provide a programming interface that allows you to implement these features yourself. For example, through a Worker, you can write logic to detect network conditions and select the best path, without relying on the default settings of the basic plan.
- Custom HTTP request handling:
Using workers, you can intercept and modify HTTP requests and responses. This allows you to forward requests to different destination IP addresses based on custom preferred IP logic, thereby achieving better performance and reliability.
Therefore, it cannot be said that CF only provides regular CDN functions in the Free plan. A more accurate description would be: in the Free plan, it quietly provides more refined edge caching functions in a relatively subtle, hard-to-detect, technically demanding, and quota-limited way (via workers). Of course, this still cannot compare with the preferred IP function; it can only be said to achieve better caching effects than Cache Rules (or Page Rules).
Additional knowledge: worker KV (Key-Value)
Earlier in the article, I mentioned that ”a worker can be simply understood as CF using the CPU and memory of its own edge network servers to help users run JS code and return the results to the users.” This raises a question: some code execution does not require caching, persistent storage, or session management, so it can run directly in the memory of CF edge servers, for example:
- HTTP Request Processing: Process, modify, and respond to HTTP requests.
- Edge Computing: Execute logic at edge nodes before requests reach the origin server.
- API Gateway: Route and process API requests.
- Static content service: Provide static files or generate dynamic content.
- Request forwarding: Forward requests to other servers for load balancing, etc.
However, the execution of some code has requirements such as caching, persistent storage, and session management, which cannot be achieved solely by CF edge server memory. Therefore, to address these needs, a supporting feature of worker is required: KV (Key-Value storage) to assist in implementation (similar to using the -v parameter in the docker run command to mount external storage for persisting specific file or folder contents).
CF worker KV is a fully managed key-value storage service provided by CF, specifically designed for CF worker. It allows developers to store and access data on globally distributed CF edge servers, which can be used by worker, enabling you to execute more complex logic and handle requests at the edge.
Some key features include:
- Global Distribution: Data is stored on edge servers in multiple locations globally, enabling low-latency access.
- Fast read and write: Provides fast read and write access, suitable for applications with high throughput and low latency requirements.
- Scalability: Supports large-scale data storage and high concurrent access, suitable for handling a large number of concurrent requests.
- Version Control: Supports version control and transactional operations, making data updates more secure and controllable.
- Security: Data is encrypted during transmission and storage, ensuring data security and privacy protection.
And as it happens, using worker to implement the ”budget version of APO for WordPress” feature requires KV to cooperate in completing the edge caching part of the operation.
Note: CF worker KV has some usage limitations in the Free plan, which include:
- Storage limits: In the Free plan, the total capacity of KV storage is limited to 1 GB. This means the total size of key-value pairs you can store cannot exceed 1 GB.
- Read count: The Free plan has a limit of 100,000 read operations (reads) per month. Once this limit is exceeded, additional read operations will be charged.
- Write count: The Free plan has a limit of 1,000 write operations (writes) per month. Once this limit is exceeded, additional write operations will be charged.
- List operations: List operations (listing keys) are limited to 1,000 times per month.
- Performance: The synchronization of KV data in the Free plan to all global data centers is slower and has higher latency. Premium plans (such as Pro or Enterprise) offer faster synchronization speeds and lower latency.
- API request rate: In the Free plan, the allowed API request rate per second is lower, while premium plans allow higher rates.
Overall, these limitations make the KV of the Free plan only suitable for small project development and testing, but well, it is enough for personal blogs.
Using CF worker to implement the ”poor man's APO for WordPress” feature for WordPress
Background introduction: APO For WordPress and Edge Cache HTML
The Automatic Platform Optimization (APO) for WordPress feature, officially launched by CF on October 1, 2020, aims to optimize the performance of WordPress websites, reduce loading times, and improve user experience through CF's global Content Delivery Network (CDN). APO automatically caches and optimizes the content of WordPress websites, including HTML, JavaScript, CSS, and images, thereby accelerating page loading and reducing server load. As for this optimization, CF's official blog once mentioned that it is actually accomplished through worker:

APO for WordPress itself is a paid service ($5/month), which is still a bit expensive for most personal webmasters (after all, most personal webmasters nowadays do it out of love, saving wherever they can, and usually don't have much traffic, so paying is not very cost-effective). However, since APO for WordPress can use worker for optimization, can users on the Free plan also use worker to optimize access to their WordPress sites?
The answer is yes, thanks to a JS script demonstration template previously released by CF: edge-cache-html.js

This JS script can work with content management systems (such as WordPress) that support the x-HTML-Edge-Cache header via plugins, or it can work directly with most static-type sites using edge cachingfor non-logged-in usersto edge-cache HTML. Therefore, to use worker to implement the budget version of the APO feature later, you will need to modify this JS demonstration script as a template. In addition, CF also provides a companion plugin for WordPress (to support the x-HTML-Edge-Cache header). Isn't that thoughtful?
Note 1: APO for WordPress also adopts the plugin + worker model, except that the plugin is the official cloudflare plugin, and the worker automatically takes effect in the system background without manual setup and intervention.
Note 2: To use this JS script, you must bind the key/value namespace to this Worker script using the EDGE_CACHE variable, which can be seen in the code below. You can keep an eye on it, as it will be used later.
Note 3: The following code is only for WordPress. For JS code targeting general static websites, please refer to the second-to-last chapter.
The following is the annotated version of the script modified for WordPress sites. Compared to the official template, I have made some adjustments to the variables and optimized it for WordPress cacheable content (I have already explained which WordPress content needs to bypass caching in Series Tutorial 6, so I won't repeat it here):
// 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.
// 定义需要处理的缓存头部信息
const CACHE_HEADERS = ['Cache-Control', 'Expires', 'Pragma'];
// 默认的绕过缓存的 Cookie 前缀
const DEFAULT_BYPASS_COOKIES = [
"wp-",
"wordpress",
"_logged_in_",
"comment_",
"woocommerce_"
];
// 使用正则表达式定义需要绕过缓存的URL
const BYPASS_URL_PATTERNS = [
/\/wp-admin\/.*/,
/\/wp-adminlogin\/.*/,
/\/wp-login\/.*/,
/\/wp-comment\/.*/,
/xmlrpc.*/,
/preview=true.*/,
/[?&]s=.*/
];
// 监听 fetch 事件,处理每个请求
addEventListener("fetch", event => {
// 在发生异常时继续传递事件
event.passThroughOnException();
// 处理请求
event.respondWith(handleRequest(event));
});
// 主请求处理函数
async function handleRequest(event) {
const { request } = event;
// 如果请求应绕过缓存,则直接获取请求
if (shouldBypassRequest(request)) {
return fetch(request);
}
// 获取缓存的响应
let { response, cacheVer, status } = await getCachedResponse(request);
// 如果没有缓存响应,则获取并缓存响应
if (!response) {
response = await fetchAndCache(request, cacheVer, event);
} else {
status = 'HIT';
// 更新缓存(如果需要)
event.waitUntil(updateCacheIfNeeded(request, cacheVer, event));
}
// 增强响应头
return enhanceResponse(response, status, cacheVer);
}
// 判断请求是否应绕过缓存
function shouldBypassRequest(request) {
const accept = request.headers.get('Accept');
const isImage = accept && accept.includes('image/*');
return isImage || !isConfigured() || isUpstreamCachePresent(request);
}
// 检查配置是否完整
function isConfigured() {
return typeof HTML、JavaScript、CSS 和图像EDGE_CACHE !== 'undefined' || (
CLOUDFLARE_API.email.length && CLOUDFLARE_API.key.length && CLOUDFLARE_API.zone.length
);
}
// 判断请求头中是否有上游缓存信息
function isUpstreamCachePresent(request) {
return request.headers.get('x-HTML-Edge-Cache') !== null;
}
// 获取缓存的响应
async function getCachedResponse(request) {
if (!shouldCacheRequest(request)) {
return { response: null, cacheVer: null, status: 'Miss' };
}
const cacheVer = await getCurrentCacheVersion(null);
const cacheKeyRequest = generateCacheRequest(request, cacheVer);
const cache = caches.default;
let response = await cache.match(cacheKeyRequest);
if (response) {
response = new Response(response.body, response);
if (shouldBypassEdgeCache(request, response)) {
return { response: null, cacheVer, status: 'Bypass Cookie' };
}
response = cleanupCacheHeaders(response);
return { response, cacheVer, status: 'Hit' };
}
return { response: null, cacheVer, status: 'Miss' };
}
// 判断请求是否应缓存
function shouldCacheRequest(request) {
const accept = request.headers.get('Accept');
return request.method === 'GET' && accept && accept.includes('text/html');
}
// 清理缓存头部信息
function cleanupCacheHeaders(response) {
CACHE_HEADERS.forEach(header => {
const value = response.headers.get(x-HTML-Edge-Cache-Header-${header});x-HTML-Edge-Cache-Header-${header});
}
});
return response;
}
// 获取并缓存响应
async function fetchAndCache(request, cacheVer, event) {
const response = await fetch(modifyRequest(request));
if (shouldCacheResponse(request, response)) {
const status = await cacheResponse(cacheVer, request, response, event);
response.headers.set('x-HTML-Edge-Cache-Status', status);
}
return response;
}
// 修改请求头部以包含缓存支持信息
function modifyRequest(request) {
const newRequest = new Request(request);
newRequest.headers.set('x-HTML-Edge-Cache', 'supports=cache|purgeall|bypass-cookies');
return newRequest;
}
// 判断响应是否应缓存
function shouldCacheResponse(request, response) {
const accept = request.headers.get('Accept');
return request.method === 'GET' && response.status === 200 && accept && accept.includes('text/html');
}
// 缓存响应
async function cacheResponse(cacheVer, request, response, event) {
const cache = caches.default;
const cacheKeyRequest = generateCacheRequest(request, cacheVer);
const clonedResponse = new Response(response.body, response);
CACHE_HEADERS.forEach(header => {
const value = clonedResponse.headers.get(header);
if (value) {
clonedResponse.headers.set(x-HTML-Edge-Cache-Header-${header}, value);
clonedResponse.headers.delete(header);
}
});
clonedResponse.headers.set('Cache-Control', 'public; max-age=315360000');
event.waitUntil(cache.put(cacheKeyRequest, clonedResponse));
return "Cached";
}
// 根据需要更新缓存
async function updateCacheIfNeeded(request, cacheVer, event) {
if (shouldCacheRequest(request)) {
const response = await fetch(modifyRequest(request));
if (shouldCacheResponse(request, response)) {
await cacheResponse(cacheVer, request, response, event);
}
}
}
// 增强响应头部信息
function enhanceResponse(response, status, cacheVer) {
const newResponse = new Response(response.body, response);
if (status) {
newResponse.headers.set('x-HTML-Edge-Cache-Status', status);
}
if (cacheVer !== null) {
newResponse.headers.set('x-HTML-Edge-Cache-Version', cacheVer.toString());
}
return newResponse;
}
// 获取当前缓存版本
async function getCurrentCacheVersion(cacheVer) {
if (cacheVer === null) {
if (typeof EDGE_CACHE !== 'undefined') {
cacheVer = await EDGE_CACHE.get('html_cache_version') || 0;
await EDGE_CACHE.put('html_cache_version', cacheVer.toString());
} else {
cacheVer = -1;
}
}
return cacheVer;
}
// 生成缓存请求
function generateCacheRequest(request, cacheVer) {
const url = new URL(request.url);
url.searchParams.append('cf_edge_cache_ver', cacheVer);
return new Request(url.toString());
}
// 判断是否应绕过边缘缓存
function shouldBypassEdgeCache(request, response) {
const url = new URL(request.url);
if (BYPASS_URL_PATTERNS.some(pattern => pattern.test(url.pathname + url.search))) {
return true;
}
const options = getResponseOptions(response);
const bypassCookies = options ? options.bypassCookies : DEFAULT_BYPASS_COOKIES;
const cookieHeader = request.headers.get('cookie');
if (cookieHeader) {
return bypassCookies.some(prefix => cookieHeader.split(';').some(cookie => cookie.trim().startsWith(prefix)));
}
return false;
}
// 获取响应选项
function getResponseOptions(response) {
const header = response.headers.get('x-HTML-Edge-Cache');
if (!header) return null;
const options = {
purge: header.includes('purgeall'),
cache: header.includes('cache'),
bypassCookies: header.split(',')
.filter(command => command.trim().startsWith('bypass-cookies'))
.flatMap(command => command.split('=')[1].split('|').map(cookie => cookie.trim()))
};
return options;
}
注释说明
• 缓存头部常量:定义需要处理的缓存头部信息。
• 默认绕过缓存的 Cookie 前缀:定义默认的绕过缓存的 Cookie 前缀列表。
• 绕过缓存的 URL 模式:定义需要绕过缓存的 URL 模式。
• 事件监听器:监听 fetch 事件,并处理每个请求。
• 主请求处理函数:处理每个请求,判断是否需要绕过缓存,并获取或缓存响应。
• 绕过缓存判断:判断请求是否需要绕过缓存。
• 配置检查:检查配置是否完整。
• 上游缓存检查:检查请求头中是否包含上游缓存信息。
• 获取缓存响应:获取缓存中的响应,如果没有缓存响应则返回空。
• 缓存请求判断:判断请求是否应缓存。
• 清理缓存头部信息:清理响应中的缓存头部信息。
• 获取并缓存响应:获取并缓存响应。
• 修改请求头部:修改请求头部以包含缓存支持信息。
• 缓存响应判断:判断响应是否应缓存。
• 缓存响应:将响应缓存到边缘缓存中。
• 根据需要更新缓存:根据需要更新缓存内容。
• 增强响应头部信息:增强响应头部信息,添加缓存状态和版本信息。
• 获取当前缓存版本:获取当前的缓存版本号。
• 生成缓存请求:生成缓存请求,附加缓存版本号。
• 判断是否绕过边缘缓存:判断是否应绕过边缘缓存,根据 URL 模式和 Cookie 前缀。
• 获取响应选项:获取响应中的选项信息,包括缓存和绕
response.headers.set(header, value);response.headers.delete(”):
// IMPORTANT: Ensure that a Key/Value Namespace (EDGE_CACHE) is bound to this worker script.
// This KV store is recommended as it allows purging only HTML instead of the entire cache.
const CACHE_HEADERS = ['Cache-Control', 'Expires', 'Pragma'];
const DEFAULT_BYPASS_COOKIES = [
"wp-",
"wordpress",
"_logged_in_",
"comment_",
"woocommerce_"
];
const BYPASS_URL_PATTERNS = [
/\/wp-admin\/.*/,
/\/wp-login\/.*/,
/xmlrpc.*/,
/preview=true.*/,
/[?&]s=.*/
];
addEventListener("fetch", event => {
event.passThroughOnException();
event.respondWith(handleRequest(event));
});
async function handleRequest(event) {
const { request } = event;
// Check if the request should bypass the cache
if (shouldBypassRequest(request)) {
return fetch(request);
}
// Attempt to get the cached response
let { response, cacheVer, status } = await getCachedResponse(request);
if (!response) {
response = await fetchAndCache(request, cacheVer, event);
} else {
status = 'HIT';
event.waitUntil(updateCacheIfNeeded(request, cacheVer, event));
}
return enhanceResponse(response, status, cacheVer);
}
function shouldBypassRequest(request) {
const accept = request.headers.get('Accept');
const isImage = accept && accept.includes('image/*');
return isImage || !isConfigured() || isUpstreamCachePresent(request);
}
function isConfigured() {
return typeof EDGE_CACHE !== 'undefined'; // Ensure KV is configured
}
function isUpstreamCachePresent(request) {
return request.headers.get('x-HTML-Edge-Cache') !== null;
}
async function getCachedResponse(request) {
if (!shouldCacheRequest(request)) {
return { response: null, cacheVer: null, status: 'Miss' };
}
const cacheVer = await getCurrentCacheVersion();
const cacheKeyRequest = generateCacheRequest(request, cacheVer);
const cache = caches.default;
let response = await cache.match(cacheKeyRequest);
if (response) {
response = new Response(response.body, response);
if (shouldBypassEdgeCache(request, response)) {
return { response: null, cacheVer, status: 'Bypass Cookie' };
}
response = cleanupCacheHeaders(response);
return { response, cacheVer, status: 'Hit' };
}
return { response: null, cacheVer, status: 'Miss' };
}
function shouldCacheRequest(request) {
const accept = request.headers.get('Accept');
return request.method === 'GET' && accept && accept.includes('text/html');
}
function cleanupCacheHeaders(response) {
CACHE_HEADERS.forEach(header => {
const value = response.headers.get(x-HTML-Edge-Cache-Header-${header});x-HTML-Edge-Cache-Header-${header});
}
});
return response;
}
async function fetchAndCache(request, cacheVer, event) {
const response = await fetch(modifyRequest(request));
if (shouldCacheResponse(request, response)) {
const status = await cacheResponse(cacheVer, request, response, event);
response.headers.set('x-HTML-Edge-Cache-Status', status);
}
return response;
}
function modifyRequest(request) {
const newRequest = new Request(request);
newRequest.headers.set('x-HTML-Edge-Cache', 'supports=cache|purgeall|bypass-cookies');
return newRequest;
}
function shouldCacheResponse(request, response) {
const accept = request.headers.get('Accept');
return request.method === 'GET' && response.status === 200 && accept && accept.includes('text/html');
}
async function cacheResponse(cacheVer, request, response, event) {
const cache = caches.default;
const cacheKeyRequest = generateCacheRequest(request, cacheVer);
const clonedResponse = new Response(response.body, response);
CACHE_HEADERS.forEach(header => {
const value = clonedResponse.headers.get(header);
if (value) {
clonedResponse.headers.set(x-HTML-Edge-Cache-Header-${header}, value);
clonedResponse.headers.delete(header);
}
});
clonedResponse.headers.set('Cache-Control', 'public; max-age=315360000'); // 10 years cache
event.waitUntil(cache.put(cacheKeyRequest, clonedResponse));
return "Cached";
}
async function updateCacheIfNeeded(request, cacheVer, event) {
if (shouldCacheRequest(request)) {
const response = await fetch(modifyRequest(request));
if (shouldCacheResponse(request, response)) {
await cacheResponse(cacheVer, request, response, event);
}
}
}
function enhanceResponse(response, status, cacheVer) {
const newResponse = new Response(response.body, response);
if (status) {
newResponse.headers.set('x-HTML-Edge-Cache-Status', status);
}
if (cacheVer !== null) {
newResponse.headers.set('x-HTML-Edge-Cache-Version', cacheVer.toString());
}
return newResponse;
}
async function getCurrentCacheVersion() {
let cacheVer = 0;
if (typeof EDGE_CACHE !== 'undefined') {
cacheVer = await EDGE_CACHE.get('html_cache_version') || 0;
} else {
cacheVer = -1; // Fallback if KV isn't bound
}
return cacheVer;
}
function generateCacheRequest(request, cacheVer) {
const url = new URL(request.url);
url.searchParams.append('cf_edge_cache_ver', cacheVer);
return new Request(url.toString());
}
function shouldBypassEdgeCache(request, response) {
const url = new URL(request.url);
if (BYPASS_URL_PATTERNS.some(pattern => pattern.test(url.pathname + url.search))) {
return true;
}
const options = getResponseOptions(response);
const bypassCookies = options ? options.bypassCookies : DEFAULT_BYPASS_COOKIES;
const cookieHeader = request.headers.get('cookie');
if (cookieHeader) {
return bypassCookies.some(prefix => cookieHeader.split(';').some(cookie => cookie.trim().startsWith(prefix)));
}
return false;
}
function getResponseOptions(response) {
const header = response.headers.get('x-HTML-Edge-Cache');
if (!header) return null;
const options = {
purge: header.includes('purgeall'),
cache: header.includes('cache'),
bypassCookies: header.split(',')
.filter(command => command.trim().startsWith('bypass-cookies'))
.flatMap(command => command.split('=')[1].split('|').map(cookie => cookie.trim()))
};
return options;
}
CF configuration steps
Step 1: Create KV
x-HTML-Edge-Cache-Header-${header}
The following is the clean code version without comments, which will be used when creating the worker later (it seems there might be formatting issues after converting to HTML, and copying it directly into the worker might cause errors. You can try it first (if there are issues, you can directly ask ChatGPT to optimize the formatting). I also provide my own download address here, where you can directly download the txt file. The link is as follows:

Invincible file sharing, access password: ”blog.tangwudi.com”

As mentioned earlier, to use worker to control edge caching, you need to cooperate with the KV feature. Therefore, the first step is to create a KV namespace. Follow the process below to create the KV space.

Step 2: Create worker
From ”Workers & Pages” - “KV”, click ”Create namespace”:

Fill in the namespace name and click ”Add”:

Fill in the worker name and click ”Deploy” in the bottom right corner:

Then click ”Edit Code” in the red box in the top right corner:

Delete the default content in the red box on the left, then paste the uncommented clean code from earlier, and finally click ”Deploy” in the top right corner:

Click ”Save and deploy”:

Step 3: Bind the created worker and KV namespace
From ”Workers & Pages” - “Overview”, click to enter the worker you just created:

In the top-left menu under ”Settings” - “Variables” - “KV Namespace Bindings”, click ”Add binding”:

Fill in the variable name:
EDGE_CACHE
Select the previously created KV space from the KV namespace dropdown menu, then click ”Deploy” in the bottom right corner:

Step 4: Add worker route
After creating the KV and worker and completing the binding, you need to tell CF which traffic this worker and KV combination is used to handle. This requires using the worker's routing function (forwarding traffic destined for a specific URL to a specific worker).
Note: Why is this feature called worker routing? Think about the definition of traditional routing: forwarding packets whose destination IP belongs to a specific destination subnet to a specific gateway. Isn't it similar?
In the top-left menu under ”Settings” - “Triggers” - “Routes”, click ”Add route”:


As mentioned earlier, to use worker to control edge caching, you need to cooperate with the KV feature. Therefore, the first step is to create a KV namespace. Follow the process below to create the KV space.

In addition, the worker has another option, Custom Domains:

This feature allows you to bind a custom domain name (which is your domain hosted on CF) to the worker, so you can directly use your own domain to access the worker script. This is typically suitable for scenarios where you use a worker to implement functions like ”xx proxy” and need direct access.
Unlike custom domains, the worker involved in worker routing does not need to be accessed directly; CF just needs to route the corresponding traffic to the worker for processing based on specific URL paths, query parameters, and other conditions.
Why did I suddenly mention custom domains? Because although it is not used in this article, it is often encountered in other worker use cases. Although worker routing and custom domains have different uses, they share similarities: both provide control over how requests are handled. Worker routing is control based on paths and conditions, while worker custom domains are control based on domain names.
Step 5: Bypass cache for requests that need to be processed by worker
In the introduction, I mentioned that for a worker to process the corresponding traffic, the prerequisite is that the traffic must not be hit and processed by rules of functional modules located before the worker in the traffic sequence. In this article, this mainly refers to cache-related rules (of course, including WAF, there must be no rules blocking access requests). So, to ensure that requests accessing the target URL (in this article, it isblog.tangwudi.xyz/*) bypass the cache, the simplest way is through Page Rules (Page Rules were briefly mentioned when discussing Cache Rules in Part 6 of the tutorial series, so I won't repeat it here), configured as follows:


At this point, the configuration on CF is complete.
Install the ”Cloudflare Page Cache” plugin on WordPress
First download the plugin, which can be downloaded directly from the official GitHub address:GitHub official download link, if you cannot access GitHub, you can also download it from my blog's dedicated sharing site:Wudi File Sharing(Access password: blog.tangwudi.com):

Then just install and enable the plugin on WordPress, no settings are required:


Note: This plugin has never been upgraded. As the WordPress version continues to rise, I don't know when it will fail. If it fails, the result will be that the cache version management function inside it will be gone (other functions are still there, such as caching dynamic content as static HTML content). In plain English, after modifying articles or pages, the content in the cache may not be automatically refreshed, requiring manual refresh. Alternatively, consider optimizing the worker code to add cache version control logic, completely discarding the plugin and using the worker to complete the version management function. I made an initial version, and interested friends can verify it themselves:
// 缓存相关的头部字段,用于清理或转移缓存中的响应头
const CACHE_HEADERS = ['Cache-Control', 'Expires', 'Pragma'];
// 默认的绕过缓存的 Cookie 列表(通常用于用户相关的数据)
const DEFAULT_BYPASS_COOKIES = [
"wp-",
"wordpress",
"_logged_in_",
"comment_",
"woocommerce_"
];
// 需要绕过缓存的 URL 模式(例如,后台管理、登录页面等)
const BYPASS_URL_PATTERNS = [
/\/wp-admin\/.*/,
/\/wp-login\/.*/,
/xmlrpc.*/,
/preview=true.*/,
/[?&]s=.*/ // 搜索查询或预览参数
];
// 初始化缓存版本
let cacheVersion = 0; // 初始缓存版本
// 监听 fetch 事件(处理传入的 HTTP 请求)
addEventListener("fetch", event => {
event.passThroughOnException(); // 如果发生异常,继续处理
event.respondWith(handleRequest(event)); // 处理传入请求
});
// 主要的请求处理函数
async function handleRequest(event) {
const { request } = event;
// 检查请求是否需要绕过缓存(例如,图片或带有特定 Cookie 的请求)
if (shouldBypassRequest(request)) {
return fetch(request); // 如果绕过缓存,直接去抓取请求
}
// 尝试获取缓存的响应
let { response, status } = await getCachedResponse(request);
if (!response) {
// 如果缓存没有命中,则抓取并缓存响应
response = await fetchAndCache(request, event);
} else {
// 如果缓存命中,检查是否需要更新缓存
status = 'HIT';
event.waitUntil(updateCacheIfNeeded(request, event));
}
// 增强响应,添加缓存状态和版本信息
return enhanceResponse(response, status, cacheVersion);
}
// 判断请求是否需要绕过缓存
function shouldBypassRequest(request) {
const accept = request.headers.get('Accept');
const isImage = accept && accept.includes('image/*'); // 判断请求是否是图片
return isImage || !isConfigured() || isUpstreamCachePresent(request); // 如果是图片请求或已有上游缓存则绕过
}
// 判断 KV 存储(EDGE_CACHE)是否已配置(如果使用 KV,则会用到缓存版本控制)
function isConfigured() {
return typeof EDGE_CACHE !== 'undefined'; // 确保 KV 存储已配置
}
// 判断请求是否已经包含来自上游缓存的头部信息
function isUpstreamCachePresent(request) {
return request.headers.get('x-HTML-Edge-Cache') !== null;
}
// 尝试获取缓存的响应
async function getCachedResponse(request) {
// 如果请求不应该缓存(不是 HTML 或方法不是 GET),则跳过缓存
if (!shouldCacheRequest(request)) {
return { response: null, status: 'Miss' };
}
const cacheKeyRequest = generateCacheRequest(request, cacheVersion); // 基于版本生成唯一的缓存键
const cache = caches.default;
// 尝试从缓存中查找响应
let response = await cache.match(cacheKeyRequest);
if (response) {
response = new Response(response.body, response); // 克隆缓存响应
// 检查缓存是否应绕过(根据 Cookie 或 URL 模式)
if (shouldBypassEdgeCache(request, response)) {
return { response: null, status: 'Bypass Cookie' };
}
// 清理缓存头并返回响应
response = cleanupCacheHeaders(response);
return { response, status: 'Hit' };
}
return { response: null, status: 'Miss' }; // 缓存未命中,未找到响应
}
// 判断请求是否应该缓存(只对 GET 请求且是 HTML 内容)
function shouldCacheRequest(request) {
const accept = request.headers.get('Accept');
return request.method === 'GET' && accept && accept.includes('text/html');
}
// 清理缓存头,将原始的缓存相关头部转移到响应中
function cleanupCacheHeaders(response) {
CACHE_HEADERS.forEach(header => {
const value = response.headers.get(`x-HTML-Edge-Cache-Header-{header}`);
if (value) {
response.headers.set(header, value); // 设置原始头部
response.headers.delete(`x-HTML-Edge-Cache-Header-{header}`); // 删除自定义头部
}
});
return response;
}
// 获取响应并缓存(如果符合缓存条件)
async function fetchAndCache(request, event) {
const response = await fetch(modifyRequest(request)); // 修改请求,添加缓存控制头
if (shouldCacheResponse(request, response)) {
const status = await cacheResponse(request, response, event); // 缓存响应
response.headers.set('x-HTML-Edge-Cache-Status', status); // 设置缓存状态头
}
return response;
}
// 修改请求头,添加缓存控制信息
function modifyRequest(request) {
const newRequest = new Request(request);
newRequest.headers.set('x-HTML-Edge-Cache', 'supports=cache|purgeall|bypass-cookies'); // 添加自定义头部用于缓存控制
return newRequest;
}
// 判断响应是否应缓存(仅对于状态码 200 和 HTML 响应)
function shouldCacheResponse(request, response) {
const accept = request.headers.get('Accept');
return request.method === 'GET' && response.status === 200 && accept && accept.includes('text/html');
}
// 缓存响应,并根据版本号存储
async function cacheResponse(request, response, event) {
const cache = caches.default;
const cacheKeyRequest = generateCacheRequest(request, cacheVersion); // 根据缓存版本生成缓存键
const clonedResponse = new Response(response.body, response);
// 将缓存相关的头部转移到克隆的响应中
CACHE_HEADERS.forEach(header => {
const value = clonedResponse.headers.get(header);
if (value) {
clonedResponse.headers.set(`x-HTML-Edge-Cache-Header-${header}`, value);
clonedResponse.headers.delete(header);
}
});
clonedResponse.headers.set('Cache-Control', 'public; max-age=315360000'); // 设置长期缓存(10年)
event.waitUntil(cache.put(cacheKeyRequest, clonedResponse)); // 存储到缓存中
return "Cached"; // 返回缓存状态
}
// 如果需要,更新缓存(重新获取并存储新版本)
async function updateCacheIfNeeded(request, event) {
if (shouldCacheRequest(request)) {
const response = await fetch(modifyRequest(request));
if (shouldCacheResponse(request, response)) {
await cacheResponse(request, response, event); // 缓存新的响应
}
}
}
// 增强响应,添加缓存状态和版本号信息到头部
function enhanceResponse(response, status, cacheVer) {
const newResponse = new Response(response.body, response);
if (status) {
newResponse.headers.set('x-HTML-Edge-Cache-Status', status); // 添加缓存状态头
}
if (cacheVer !== null) {
newResponse.headers.set('x-HTML-Edge-Cache-Version', cacheVer.toString()); // 添加缓存版本头
}
return newResponse;
}
// 根据请求和缓存版本生成唯一的缓存键
function generateCacheRequest(request, cacheVer) {
const url = new URL(request.url);
url.searchParams.append('cf_edge_cache_ver', cacheVer); // 将缓存版本添加到 URL 中
return new Request(url.toString());
}
// 判断缓存是否应绕过(根据 URL 模式或 Cookie)
function shouldBypassEdgeCache(request, response) {
const url = new URL(request.url);
if (BYPASS_URL_PATTERNS.some(pattern => pattern.test(url.pathname + url.search))) {
return true; // 如果 URL 匹配某个绕过模式,则绕过缓存
}
// 检查绕过的 Cookie
const options = getResponseOptions(response);
const bypassCookies = options ? options.bypassCookies : DEFAULT_BYPASS_COOKIES;
const cookieHeader = request.headers.get('cookie');
if (cookieHeader) {
return bypassCookies.some(prefix => cookieHeader.split(';').some(cookie => cookie.trim().startsWith(prefix)));
}
return false; // 不绕过缓存
}
// 解析响应中的 'x-HTML-Edge-Cache' 头部,获取缓存控制选项
function getResponseOptions(response) {
const header = response.headers.get('x-HTML-Edge-Cache');
if (!header) return null;
const options = {
purge: header.includes('purgeall'), // 检查是否设置了 'purgeall'
cache: header.includes('cache'), // 检查是否设置了 'cache'
bypassCookies: header.split(',')
.filter(command => command.trim().startsWith('bypass-cookies')) // 解析绕过的 Cookie
.flatMap(command => command.split('=')[1].split('|').map(cookie => cookie.trim()))
};
return options;
}
// 手动触发缓存版本更新(例如通过 API 或 UI 触发)
function updateCacheVersion() {
// 在这里添加你的缓存版本更新逻辑,或者暂时返回一个简单的消息
console.log("Cache version update triggered.");
}
Verify caching effect
Verify if edge cache is working
Reminder again: Test in a logged-out state of WordPress, as the logged-in state will bypass the cache.
Use Edge or Chrome browser, open Developer Tools, select ”Network”, check ”Disable cache”, and then open the target URL, which is ”blog.tangwudi.xyz” in this article:

The cache version number in the image above can be viewed in the corresponding KV namespace in the CF console (when the WordPress site content changes, the plugin will notify CF, and the edge cache version number will change at this time):


Verify if site page load time is shortened
View the timing in the developer tools under three different CF settings respectively.
Worker-based budget version of APO for WordPress
Waiting for server response time (Waiting TTFB) 332.52 ms:

Direct origin pull without using any cache
How long would it take without worker optimization? Let's verify by turning on ”Development Mode” on CF:

Revisit
https://blog.tangwudi.xyz:
Waiting for server response time (Waiting TTFB) 2.74 seconds:

Using Cache Rules
So, how does worker cache compare to using Cache Rules cache?
Waiting for server response time (Waiting TTFB) 1.14 seconds:

Direct origin pull without cache: 2.74 seconds, using Cache Rules: 1.14 seconds, using worker-based budget version of APO for WordPress: 332.52 ms. The budget version of APO for WordPress wins hands down, but Cache Rules' 1.14 seconds is also much better than 2.74 seconds, barely acceptable.
However, everyone's origin server location, origin pull method, and bandwidth used are different, so the test results of the three methods will definitely vary. Generally speaking, China Telecom and China Unicom are still much better than China Mobile.
JS code suitable for static sites
The code at the beginning of the article is only suitable for WordPress and is not very suitable for general static sites, so a code template suitable for static sites is also attached here.
JS code template suitable for most regular static sites
Mainly implements caching of HTML, JavaScript, CSS, and images (excluding static sites that require login, especially those involving user sessions or personalized content):
const CACHE_HEADERS = ['Cache-Control', 'Expires', 'Pragma'];
const BYPASS_URL_PATTERNS = [
/\/admin\/.*/, // 绕过后台管理部分页面的缓存,可以根据需要修改或移除
];
addEventListener("fetch", event => {
event.passThroughOnException();
event.respondWith(handleRequest(event));
});
async function handleRequest(event) {
const { request } = event;
if (shouldBypassRequest(request)) {
return fetch(request);
}
let { response, cacheVer, status } = await getCachedResponse(request);
if (!response) {
response = await fetchAndCache(request, cacheVer, event);
} else {
status = 'HIT';
event.waitUntil(updateCacheIfNeeded(request, cacheVer, event));
}
return enhanceResponse(response, status, cacheVer);
}
function shouldBypassRequest(request) {
const accept = request.headers.get('Accept');
const isImage = accept && accept.includes('image/*');
return isImage || isUpstreamCachePresent(request);
}
function isUpstreamCachePresent(request) {
return request.headers.get('x-HTML-Edge-Cache') !== null;
}
async function getCachedResponse(request) {
if (!shouldCacheRequest(request)) {
return { response: null, cacheVer: null, status: 'Miss' };
}
const cacheVer = await getCurrentCacheVersion(null);
const cacheKeyRequest = generateCacheRequest(request, cacheVer);
const cache = caches.default;
let response = await cache.match(cacheKeyRequest);
if (response) {
response = new Response(response.body, response);
response = cleanupCacheHeaders(response);
return { response, cacheVer, status: 'Hit' };
}
return { response: null, cacheVer, status: 'Miss' };
}
function shouldCacheRequest(request) {
const accept = request.headers.get('Accept');
return request.method === 'GET' && (
accept.includes('text/html') ||
accept.includes('application/javascript') ||
accept.includes('text/css') ||
accept.includes('image/*')
);
}
function cleanupCacheHeaders(response) {
CACHE_HEADERS.forEach(header => {
const value = response.headers.get(x-HTML-Edge-Cache-Header-${header});x-HTML-Edge-Cache-Header-${header});
}
});
return response;
}
async function fetchAndCache(request, cacheVer, event) {
const response = await fetch(modifyRequest(request));
if (shouldCacheResponse(request, response)) {
const status = await cacheResponse(cacheVer, request, response, event);
response.headers.set('x-HTML-Edge-Cache-Status', status);
}
return response;
}
function modifyRequest(request) {
const newRequest = new Request(request);
newRequest.headers.set('x-HTML-Edge-Cache', 'supports=cache|purgeall');
return newRequest;
}
function shouldCacheResponse(request, response) {
const accept = request.headers.get('Accept');
return request.method === 'GET' && response.status === 200 && (
accept.includes('text/html') ||
accept.includes('application/javascript') ||
accept.includes('text/css') ||
accept.includes('image/*')
);
}
async function cacheResponse(cacheVer, request, response, event) {
const cache = caches.default;
const cacheKeyRequest = generateCacheRequest(request, cacheVer);
const clonedResponse = new Response(response.body, response);
CACHE_HEADERS.forEach(header => {
const value = clonedResponse.headers.get(header);
if (value) {
clonedResponse.headers.set(x-HTML-Edge-Cache-Header-${header}, value);
clonedResponse.headers.delete(header);
}
});
clonedResponse.headers.set('Cache-Control', 'public; max-age=315360000');
event.waitUntil(cache.put(cacheKeyRequest, clonedResponse));
return "Cached";
}
async function updateCacheIfNeeded(request, cacheVer, event) {
if (shouldCacheRequest(request)) {
const response = await fetch(modifyRequest(request));
if (shouldCacheResponse(request, response)) {
await cacheResponse(cacheVer, request, response, event);
}
}
}
function enhanceResponse(response, status, cacheVer) {
const newResponse = new Response(response.body, response);
if (status) {
newResponse.headers.set('x-HTML-Edge-Cache-Status', status);
}
if (cacheVer !== null) {
newResponse.headers.set('x-HTML-Edge-Cache-Version', cacheVer.toString());
}
return newResponse;
}
async function getCurrentCacheVersion(cacheVer) {
if (cacheVer === null) {
cacheVer = -1;
}
return cacheVer;
}
function generateCacheRequest(request, cacheVer) {
const url = new URL(request.url);
url.searchParams.append('cf_edge_cache_ver', cacheVer);
return new Request(url.toString());
}
JS code template suitable for static sites that require login, involve user sessions, or personalized content
const CACHE_HEADERS = ['Cache-Control', 'Expires', 'Pragma'];
const BYPASS_URL_PATTERNS = [
/\/admin\/.*/,
/\/login\/.*/,
/\/account\/.*/
];
const BYPASS_COOKIES = [
'session_id', // 假设使用这个Cookie来标识登录状态
'auth_token'
];
addEventListener("fetch", event => {
event.passThroughOnException();
event.respondWith(handleRequest(event));
});
async function handleRequest(event) {
const { request } = event;
if (shouldBypassRequest(request)) {
return fetch(request);
}
let { response, cacheVer, status } = await getCachedResponse(request);
if (!response) {
response = await fetchAndCache(request, cacheVer, event);
} else {
status = 'HIT';
event.waitUntil(updateCacheIfNeeded(request, cacheVer, event));
}
return enhanceResponse(response, status, cacheVer);
}
function shouldBypassRequest(request) {
const accept = request.headers.get('Accept');
const isImage = accept && accept.includes('image/*');
if (isImage || shouldBypassCookies(request)) {
return true;
}
return BYPASS_URL_PATTERNS.some(pattern => pattern.test(request.url));
}
function shouldBypassCookies(request) {
const cookieHeader = request.headers.get('Cookie');
if (cookieHeader) {
return BYPASS_COOKIES.some(cookie => cookieHeader.includes(cookie));
}
return false;
}
async function getCachedResponse(request) {
if (!shouldCacheRequest(request)) {
return { response: null, cacheVer: null, status: 'Miss' };
}
const cacheVer = await getCurrentCacheVersion(null);
const cacheKeyRequest = generateCacheRequest(request, cacheVer);
const cache = caches.default;
let response = await cache.match(cacheKeyRequest);
if (response) {
response = new Response(response.body, response);
response = cleanupCacheHeaders(response);
return { response, cacheVer, status: 'Hit' };
}
return { response: null, cacheVer, status: 'Miss' };
}
function shouldCacheRequest(request) {
const accept = request.headers.get('Accept');
return request.method === 'GET' && (
accept.includes('text/html') ||
accept.includes('application/javascript') ||
accept.includes('text/css') ||
accept.includes('image/*')
) && !shouldBypassCookies(request);
}
function cleanupCacheHeaders(response) {
CACHE_HEADERS.forEach(header => {
const value = response.headers.get(x-HTML-Edge-Cache-Header-${header});x-HTML-Edge-Cache-Header-${header});
}
});
return response;
}
async function fetchAndCache(request, cacheVer, event) {
const response = await fetch(modifyRequest(request));
if (shouldCacheResponse(request, response)) {
const status = await cacheResponse(cacheVer, request, response, event);
response.headers.set('x-HTML-Edge-Cache-Status', status);
}
return response;
}
function modifyRequest(request) {
const newRequest = new Request(request);
newRequest.headers.set('x-HTML-Edge-Cache', 'supports=cache|purgeall');
return newRequest;
}
function shouldCacheResponse(request, response) {
const accept = request.headers.get('Accept');
return request.method === 'GET' && response.status === 200 && (
accept.includes('text/html') ||
accept.includes('application/javascript') ||
accept.includes('text/css') ||
accept.includes('image/*')
) && !shouldBypassCookies(request);
}
async function cacheResponse(cacheVer, request, response, event) {
const cache = caches.default;
const cacheKeyRequest = generateCacheRequest(request, cacheVer);
const clonedResponse = new Response(response.body, response);
CACHE_HEADERS.forEach(header => {
const value = clonedResponse.headers.get(header);
if (value) {
clonedResponse.headers.set(x-HTML-Edge-Cache-Header-${header}, value);
clonedResponse.headers.delete(header);
}
});
clonedResponse.headers.set('Cache-Control', 'public; max-age=315360000');
event.waitUntil(cache.put(cacheKeyRequest, clonedResponse));
return "Cached";
}
async function updateCacheIfNeeded(request, cacheVer, event) {
if (shouldCacheRequest(request)) {
const response = await fetch(modifyRequest(request));
if (shouldCacheResponse(request, response)) {
await cacheResponse(cacheVer, request, response, event);
}
}
}
function enhanceResponse(response, status, cacheVer) {
const newResponse = new Response(response.body, response);
if (status) {
newResponse.headers.set('x-HTML-Edge-Cache-Status', status);
}
if (cacheVer !== null) {
newResponse.headers.set('x-HTML-Edge-Cache-Version', cacheVer.toString());
}
return newResponse;
}
async function getCurrentCacheVersion(cacheVer) {
if (cacheVer === null) {
cacheVer = -1;
}
return cacheVer;
}
function generateCacheRequest(request, cacheVer) {
const url = new URL(request.url);
url.searchParams.append('cf_edge_cache_ver', cacheVer);
return new Request(url.toString());
}
关键点解释:
1. BYPASS_COOKIES:定义了一组会话或身份验证相关的Cookie,如session_id或auth_token,如果请求中包含这些Cookie,则不进行缓存。
2. shouldBypassCookies:检查请求是否包含这些会话Cookie。如果包含,则跳过缓存,并直接从源服务器获取内容。
3. 公共资源的缓存:对于不涉及用户个性化或会话的资源(如CSS、JS、图片),仍然可以进行缓存,以提升站点性能。
Precautions for caching static sites using worker method
For general static sites, there is no need to install plugins like WordPress to support x-HTML-Edge-Cache. As long as the site's file structure and path design are reasonable, the cache strategy can be handled directly in the worker script. However, the following points need to be noted:
- URL Path Design: Ensure that the URL paths of static resources (HTML, CSS, JavaScript, images, etc.) are fixed and do not depend on dynamic parameters. This ensures a higher cache hit rate.
- Response Header Settings: Although there is no need to install plugins like WordPress, on the server side, you can consider setting appropriate cache control headers (such as Cache-Control) to cooperate with the worker's cache strategy. If the static site uses a server-side (such as NGINX, Apache), you can manually configure these headers.
- Clear Cache: When static resources are updated, the cache can be cleared through the API provided by the worker or the CF panel to ensure that users get the latest content (still not as convenient as WordPress using a plugin for automatic updates).
- Features requiring no special support: For static sites that do not rely on user sessions or personalized content, the basic caching logic of the worker script is sufficient, and there is no need to install additional plugins to handle specific cache headers as in WordPress.
Summary
Although the "poor man's version" of APO for WordPress implemented using worker, KV, and the Edge Cache HTML script is decent enough to get by, there is still a certain gap compared to the genuine APO feature in terms of access speed, compatibility, performance, etc. However, for friends using the Free plan, it can still be considered a blessing.
However, the biggest limitation of this method actually comes from the free quota of 100,000 requests per day for worker in the Free plan. This quota is actually completely sufficient for average personal webmasters and can hardly be used up. But the most feared thing is: DDoS attacks. Once encountered, this 100,000 free quota will be exhausted in a short while, and then it can only go directly back to the origin (2.74 seconds), whereas APO has no limits.
Of course, there is also an emergency workaround: keep the relevant configuration of regular Cache Rules (see article:Home Data Center Series Cloudflare Tutorial (6) CF Cache Rules Feature Introduction and Detailed Configuration Tutorial), and normally use Page Rules to bypass cache (since Page Rules have a higher priority than Cache Rules in the traffic sequence, as long as a Page Rule is hit to bypass the cache, the subsequent Cache Rules will not be hit). When a DDoS attack occurs and the free quota of the worker is exhausted, as long as you manually turn off the Page Rules switch, the incoming traffic will hit the subsequent Cache Rules (Cache Rules have a higher priority than worker in the traffic sequence, 1.14 seconds, which is always better than the 2.74 seconds of going directly back to the origin). Once you survive until the next day, turning the Page Rules back on makes you good to go again.
Note 1: Don't think DDoS is far away from you. I was attacked again yesterday, though I didn't feel much:

Note 2: From this article, everyone should be able to see the importance of network basics, right? If your network foundation is not solid, in many cases you won't be able to analyze problems deeply, let alone troubleshoot some bizarre issues. However, how many friends working in network-related fields nowadays still read the ”TCP/IP Illustrated Trilogy”?
Note 3: Writing this article was so tiring. I don't want to write academic-style articles involving so many technical points and details in the short term. I'll just write some casual, filler articles to take a break. Fortunately, this article about worker seems to be the most troublesome one in the CF tutorial series so far; the subsequent ones won't be this exhausting.
AI prompts that there is a syntax error, the correct way to write it:
Should put x-HTML-Edge-Cache-Header-{header} into backticks (``), and use{} to reference template variables.
This code was also optimized by ChatGPT before... It feels like its ideas change every once in a while. This code definitely runs fine, I have verified it, but you can also let AI optimize it further, as long as it runs normally in the end. With AI, you can optimize code however you want, as long as it works. You can also directly use the official script on GitHub as a blueprint and let AI optimize it.
Bro, there is an issue with your CF worker script. Copying and pasting it in results in a bunch of errors, and then CF won't let me save it.
That shouldn't be the case, I verified it. The test results in the article are all based on the script.
It might be a formatting issue. Copying my local code directly to the worker works fine, but copying the code from the website indeed throws errors. Let me adjust it.
I have temporarily provided the download link for the code in the article. The txt files for both the WordPress version and the HTML version of the code have been uploaded (also put in the comments: the link is as follows:Invincible File Sharing, access password: ”blog.tangwudi.com”), you can download it directly. I will look into the formatting issue when I have some free time.
The worker is on the edge node, and the cache is also on the edge node. Why is using the worker as a cache faster than the regular cache?
It's so troublesome now, I don't receive email notifications for comments anymore~~ I only saw this today. I mentioned it in the section ”Why using worker to control edge cache can achieve better access results than CF's regular CDN” in the article. Actually, the worker feature has some privileges that break through some limitations of regular caching, allowing it to freely piggyback on some privileges of the paid APO feature.