Drupal should be quite fast and with its page cache, it should be enough for a lot of sites without adding any extra services. In the real world, sites are developed by different developers with different experiences, and with small issues piling up until sites will get slow.
In this article, you can find some points to look out for which I have seen, and possible fixes and workarounds.
In this article
Using the Views module

First of all - using it is just fine, but it can be bad if you use it too much and in the wrong places. Let's look at the pros and cons of using it.
Pros of using the Views module:
- Easy to create lists of content
- Filtering, pagers
- Good integration (can create blocks and pages)
Cons of the Views module:
- It can be unnecessary in half of the cases (like you don't need filters and pagers)
- Has performance issues like using node_list cache tag by default or using rewriting options (noticeable if a lot of items are rendered on a single page)
I had used the Views for years and I did use it for simple lists as well. The node_list cache tag issue was something I did not know and was an issue on one of the sites that had related content blocks generated by the Views, which invalidated most of the site with each CRUD operation. I recommend using Entity Query for simple listings instead.
Using node entity type for everything

Unless you are on Drupal 7, stop creating everything as a node bundle.
Why this would be bad:
- The admin side is a mess and hard to understand and clearly shows the quality of your work
- You don't have control over the entity and will need to use everything the node entity has configured to use
- Performance may degrade when the site is going to have a lot of content (revisions table queries will slow down)
I have used custom entities a lot and don't see any reason you should not use them. I have used this logic to determine whether the entity type I need will be a node or custom entity which is based on whether the canonical view is needed or not:
- If the content type I need has a canonical view, it probably can be a node bundle
- If the content does not have a canonical view (you can still have them) and is being embedded inside another entity or used just for some calculations later - it is most likely a custom entity.
If you are creating a custom entity, disable the canonical view if you are not using it so things are not leaking.
Misuse of blocks

The blocks in Drupal are a powerful and useful feature, but often badly used. There are two types of blocks - blocks created by plugins or the block_content module.
Common mistakes when using blocks:
- Too many blocks are used in regions
- Blocks need to have a specific order
- Site editors need to manage them so you need to allow them to modify the block layout page which will be messed up at some point
- Blocks are not used as blocks but like service classes and are called programmatically in random places which will confuse other developers and could cause performance issues as well
- Using block_content module for one thing only (single block somewhere on the page)
Blocks are fine and use as much as you need, but try to be consistent and use as few as possible. You can also combine blocks into one (for example, a single custom block for header only).
Inefficient cache tag usage
Cache tags help Drupal to understand what to invalidate and when. Out of the box, Drupal offers a lot of them already. To get the best out of them, you need to use them correctly, otherwise, things won't appear or update or you are invalidating way too much. The most abused one - ENTITY_TYPE_list
- is something you rarely should use because it will invalidate items with that tag on any CRUD action with that entity type.
Most likely you want to use it because something does not appear when new content is added - for this use hooks and custom cache tag instead to invalidate things at the correct time or if you need to use it, limit it to bundle, for example, node_list:article
. I recommend invalidating with a custom tag and only when the conditions are satisfied for that functionality.
Cron

The cron is an essential feature to set up in the Drupal site. There are many ways to do it, but the most common way is to use HTTP request or Drush. What is not recommended is to use the automated_cron module, it could be unreliable as it requires some traffic on the site.
HTTP request (i.e wget)
- It is easier to set up and does not usually require separate tools to be installed
- It can be run externally
- It can be problematic if it needs to run resource-heavy or long-running jobs where you need to increase server resource limits
- It depends on the web server and keeps one process busy
Drush
- Usually runs without any limits (PHP CLI usually does not have them)
- A reliable way to run long imports
- It does not depend on the web server
- Requires access to crontab or similar tool to run drush command periodically
- Need to know how to set up CRON jobs.
The best way is to run jobs with Drush because it has no webserver dependency and the server has more resources to handle requests on peak times. If you have a site, that has problems going down, check first how the CRON is running that site - maybe it is one contributor to that problem.
Database queries are inefficient and slow
This can be really complicated issue and depends on what the logic at the moment is - it can be that there are a lot of queries made into the database or the queries are just slow (indexes are not set, queries are complex).
First of all, try to identify the issue. To find what queries are exactly run, I have used a Devel with Webprofiler module to get an idea of what exactly is going on with the page request.
If the query is slow and there are no solutions to make it faster, make sure the cache works well. The first load to the cache can be slow, but following requests will be faster. You can also use the Warmer module to cache it after the cache rebuild.
Using sessions for anonymous users
Initiating sessions for anonymous users is also something I have seen a few times. The session will bypass the cache and some code needs to be executed every page load. This is not good, especially with other performance issues you may have on the page.
It can be initiated by trying to access the session or with a temp storage service, so to find where it was initiated a few keywords can be used to find the problematic code.
Initiating a session for anonymous users is rarely needed and in many cases, this was a workaround for some functionality the developer didn't know how to solve. Sometimes you could use cookies or browser local/session storage to store data. If the site is loading slower, check if there's a session initiated.
Expensive operations in the wrong places
Sometimes some operations take too much time and in that case, you could rely on the Drupal cache layer. The only downside is that things may be invalidated at random times and it is a good idea to do things in the correct places.
For example, you need to display some data on some template and the preprocess hook looks good for this. Before doing it, check which preprocess you are trying to use - some preprocess functions are executed more often than others - for example, hook_preprocess_page
will run all the time if something changes on the page. You do not want any complex logic there. Or some general preprocess hooks like hook_preprocess_block
will run for all blocks.
Duplication of things
There are times when it is needed to show content in different places depending on the screen resolution, for example, a mobile menu in a different container. The approach I have used and seen is to render things two times, which can take additional time. An alternative way to do the same thing is to use some Javascript to clone elements into the correct place and show one of those elements depending on the screen size. Javascript can also help alter other behavior or layout if it is a complicated or hacky way to do it on the backend. The performance is not usually an issue unless you try to do some heavy stuff.
Tracking URL parameters
Depending on the cache configuration, URL parameters may create a new cache context making Drupal render the page again. While it is logical to do this, there may be specific arguments you may want to skip. For example, different ad campaigns and sharing on social media may add some tracking parameters where you don't need to re-render the page again.
To resolve this issue, use the Page Cache Query Ignore module to exclude tracking or other arguments. This might give some additional performance boost because Drupal is not generating the page again and uses a cached version.
Unused functionality being enabled
Hoarding can be a problem whether it is a code, configuration, or functionality. Excess functionality can come from the default install profile, testing something, or removing some functionality. I consider it a bad practice and developers should clean up after themselves. It will confuse other developers and extra CPU cycles will be used for every page load. Don't be a code hoarder and clean after yourself.
Summary
When dealing with performance issues there are usually multiple contributors and it is important to fine-tune as much as possible. Drupal itself has a good caching layer and by default should work for small and medium sites just fine and no extra layers (Redis/Memcache, Varnish, Cloudfront) are not always needed.
To make Drupal perform as best as possible:
- Make sure the cache layer works as expected
- Keep an eye on performance during development
- Code must follow standards and best practices, do not do hacks
- When playing around with modules and features, clean up after yourself, including the defaults that came from the installation profile