Quick answer: you can add reading time to WordPress with a short snippet in your theme’s functions.php, or you can install a plugin. The code route costs you nothing in plugin overhead but breaks silently on theme updates and needs manual work for caching, per-post-type control, and styling. The plugin route costs a small amount of extra weight but stays maintained and configurable. Below is the actual code, a worked comparison, and where each approach falls apart in practice.
The code approach
Here’s a working snippet you can drop into your child theme’s functions.php file. It calculates reading time based on word count and returns it as a string you can echo inside your template.
function rt_calculate_reading_time() {
$content = get_post_field('post_content', get_the_ID());
$word_count = str_word_count(strip_tags($content));
$minutes = ceil($word_count / 200);
return $minutes . ' min read';
}
Then in single.php, or wherever your theme outputs the post meta, you’d add:
<span class="reading-time"><?php echo rt_calculate_reading_time(); ?></span>
That’s the whole feature, functionally. 200 words per minute is a commonly used average reading speed, though you can adjust the divisor if you want a faster or slower baseline.
What this simple version doesn’t handle
The snippet above works, but it’s missing several things a real implementation needs:
- Caching. As written, it recalculates word count on every single page load. On a cached site this usually isn’t triggered often, but on high-traffic pages without full-page caching, that’s wasted processing repeated for every visitor.
- Shortcode and block handling. Word counts from raw post content can include shortcode text or block comments that aren’t actually visible words, which quietly inflates the number.
- A progress bar. That’s an entirely separate feature requiring JavaScript, a scroll event listener, and CSS, none of which is covered by the PHP snippet above.
- Child theme survival. If you’re not using a child theme and you edit functions.php directly, a theme update wipes the code out completely.
- Settings UI. Want to change the placement, or turn it off for one post type? That means editing the code again, every time.
Extending the code version properly
If you want to fix the caching issue, you can store the result in post meta the first time it’s calculated, then read from meta afterward:
function rt_get_reading_time($post_id) {
$cached = get_post_meta($post_id, '_rt_reading_time', true);
if ($cached) {
return $cached;
}
$content = get_post_field('post_content', $post_id);
$word_count = str_word_count(strip_tags($content));
$minutes = ceil($word_count / 200);
$result = $minutes . ' min read';
update_post_meta($post_id, '_rt_reading_time', $result);
return $result;
}
You’d also want to hook into save_post to clear that cached meta whenever the post is edited, otherwise the number goes stale after you update an article. At this point you’ve written roughly what a plugin already does, just without a settings screen or a progress bar.
Cleaning up the word count for shortcodes and blocks
The stock strip_tags() call removes HTML markup but leaves shortcode brackets and their content behind, so a post with a few gallery or embed shortcodes can show a higher reading time than the visible text justifies. A more accurate version runs the content through do_shortcode() first and strips block comment markers before counting:
function rt_clean_word_count($post_id) {
$content = get_post_field('post_content', $post_id);
$content = preg_replace('/<!--(.*?)-->/s', '', $content);
$content = strip_shortcodes($content);
$content = strip_tags($content);
return str_word_count($content);
}
This is closer to what a dedicated plugin does internally, and it’s a good example of why the “just a snippet” version tends to grow over time once you notice edge cases on real posts.
A worked example
Take a site with 40 published posts, averaging 900 words each, running a full-page cache with a 12 hour expiry. With the basic uncached snippet, the reading time function runs on every uncached hit, roughly 2,000 to 3,000 times a day on a mid-traffic blog, each call doing a strip_tags and word count pass over the full post body. That’s a small but real amount of repeated PHP work that a plugin using post-meta caching or transients would only do once per post per cache cycle.
Now compare page weight. The plain PHP snippet adds zero kilobytes to the front end, since it only outputs a text string. A typical reading time plugin with a progress bar adds somewhere between 3 and 10 kilobytes of CSS and JavaScript, which on a broadband connection is not noticeable, but on a slow mobile connection is the kind of thing that shows up in a Lighthouse audit. If your site already carries a heavy theme and several other plugins, that extra few kilobytes matters more than it would on a lean setup.
The practical takeaway: the code snippet wins narrowly on front-end weight, the plugin wins on server-side efficiency once you add proper caching, and the plugin wins outright the moment you want a progress bar, since hand-rolling scroll tracking is a bigger project than the reading time calculation itself.
When the code route actually makes sense
- You already maintain a custom theme and are comfortable editing PHP directly.
- You want the absolute minimum footprint and only need the number, no progress bar, no configurable placement.
- You’re building a one-off feature for a client site where you control every update and won’t lose the code.
When a plugin makes more sense
- You want a progress bar in addition to the reading time number, since that requires JavaScript most theme developers won’t want to hand-roll.
- You want to toggle the feature on or off per post type from a settings screen instead of editing code.
- You’re not using a child theme, or you don’t want to risk losing custom code on a theme update.
- You want caching handled automatically instead of writing and maintaining your own meta-caching logic.
Installing a plugin instead
- Go to Plugins > Add New Plugin in your dashboard.
- Search for a reading time plugin, ideally one described as lightweight and cache-safe.
- Install and activate it.
- Open its settings and choose display placement, whether to include a progress bar, and which post types it applies to.
- Save and check a live post.
That’s the entire setup, and it covers the caching and progress bar gaps in the code snippet above without you having to maintain any of it yourself.
A middle ground worth knowing about
ReadyGo Reading Time is a free plugin built to stay close to the code approach in terms of weight, meaning it doesn’t load extra libraries or slow down cached pages, while still giving you the settings screen and progress bar option the raw snippet above doesn’t include. It’s a reasonable way to get plugin convenience without plugin bloat.
FAQ
Will the code snippet survive a WordPress core update? Yes, core updates don’t touch your theme files. The only thing that wipes out functions.php code is a theme update, and only if you edited the parent theme directly instead of a child theme.
Can I use the code snippet and a plugin at the same time? You can, but there’s no reason to. Run one or the other, since having both means you’re calculating and possibly displaying reading time twice.
Is 200 words per minute accurate for every site? It’s a reasonable average, but technical or dense content reads slower for most people, while casual list-style posts read faster. If your content skews heavily one way, adjusting the divisor in the code, or the setting in a plugin, gives a more honest number.
Does the code version work with the block editor? Yes. The function reads post_content directly from the database, which is the same rendered HTML whether it was written in Gutenberg or the classic editor, so there’s no compatibility issue either way.
Wrapping up
Both routes work. Code is fine if you’re comfortable in functions.php and only want the bare number. A plugin is the more practical choice the moment you want a progress bar, per-post-type control, or you simply don’t want to be the one maintaining a caching layer and shortcode edge cases by hand.
