Skip to content

perf: skip redundant work in RouteCollection::create() - #10656

Merged
lonnieezell merged 3 commits into
codeigniter4:4.8from
lonnieezell:issue-10617
Oct 24, 2026
Merged

lonnieezell merged 3 commits into
codeigniter4:4.8from
lonnieezell:issue-10617

Conversation

@lonnieezell

@lonnieezell lonnieezell commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Description
Fixes #10617

Without route caching, the route table is rebuilt on every request. This PR removes two pieces of mostly wasted work from RouteCollection::create(), which runs once per registered route:

  • Placeholder loop: the str_ireplace(':' . $tag, ...) loop now runs only when the route key contains :. Every placeholder search string starts with :, so a key without one cannot match. The output is identical.
  • helper('array'): this is now called only on the first create() of each collection, using a private flag. RouteCollection no longer uses the helper, but the helper still loads at the same point as before, so apps that rely on that side effect keep working.

There are no public or protected API changes, and the route table is unchanged (same keys, handlers, names and options).

Benchmark

Time to build the route table on 4.8: PHP 8.3.23 CLI on macOS, OPcache on, production mode, a new RouteCollection per build. The routes are a realistic mix: a third static, a third with one placeholder, a third with two, in groups of 10. Before and after were run alternately for 4 rounds. Each round takes the median of 7 timed runs, and the table shows the median across rounds.

Routes Before After Saved Per route
30 77.8 µs 65.7 µs 16% 2.59 → 2.19 µs
100 228.3 µs 189.9 µs 17% 2.28 → 1.90 µs
300 685.9 µs 556.0 µs 19% 2.29 → 1.85 µs
1000 2,309 µs 1,814 µs 21% 2.31 → 1.81 µs

Comparing the fastest round on each side gives similar savings (14–19%). The same benchmark on develop showed 17–19% savings (2.05 → 1.69 µs per route).

The issue estimated about 40%, but that assumed every route skips the placeholder loop. In this mix only the static third does, so the expected saving is about 0.5 µs × ⅓ + 0.24 µs ≈ 0.41 µs per route, which is close to the 0.4–0.5 µs measured. Apps with mostly static routes will save more.

Benchmark script
<?php

declare(strict_types=1);

$_SERVER['CI_ENVIRONMENT'] = 'production';
require getcwd() . '/system/Test/bootstrap.php';

use CodeIgniter\Router\RouteCollection;
use Config\Modules;
use Config\Routing;

function build(RouteCollection $routes, int $n): void
{
    $perGroup = 10;

    for ($g = 0; $g < intdiv($n, $perGroup); $g++) {
        $routes->group("grp{$g}", static function (RouteCollection $routes) use ($g, $perGroup): void {
            for ($i = 0; $i < $perGroup; $i++) {
                $k = $g * $perGroup + $i;

                match ($k % 3) {
                    0 => $routes->get("static{$k}/page", "Ctrl{$g}::m{$i}"),
                    1 => $routes->get("one{$k}/(:num)", "Ctrl{$g}::m{$i}/$1"),
                    2 => $routes->post("two{$k}/(:segment)/(:num)", "Ctrl{$g}::m{$i}/$1/$2"),
                };
            }
        });
    }
}

$modules          = new Modules();
$modules->enabled = false;
$routing          = new Routing();

foreach ([30, 100, 300, 1000] as $n) {
    $iterations = max(200, intdiv(200_000, $n));
    $samples    = [];

    for ($run = 0; $run < 7; $run++) {
        $start = hrtime(true);

        for ($it = 0; $it < $iterations; $it++) {
            $routes = new RouteCollection(service('locator'), $modules, $routing);
            build($routes, $n);
        }

        $samples[] = (hrtime(true) - $start) / $iterations / 1000;
    }

    sort($samples);
    printf("%5d routes: %8.1f µs/build  %5.2f µs/route\n", $n, $samples[3], $samples[3] / $n);
}

Run it from the repo root with php -d opcache.enable_cli=1 bench_routes.php.

Tests

New tests in RouteCollectionTest lock in current behavior. All three pass both before and after the change:

  • A route with no placeholders, including a raw regex, keeps its exact key.
  • A custom placeholder containing (?:...), mixed with built-in placeholders, still expands correctly.
  • Placeholders still match case-insensitively and without parentheses ((:NUM), :segment). The : guard relies on this behavior.

No changelog or user guide entry is included, as planned in the issue.

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value (without duplication)
  • Unit testing, with >80% coverage
  • User guide updated (not needed: no user-facing change)
  • Conforms to style guide

🤖 Generated with Claude Code

…eate()`

Run the placeholder `str_ireplace` loop only when the route key contains
`:`, and call `helper('array')` only on the first `create()` per
collection. The route table output is unchanged.

Route table build, PHP 8.3, OPcache on, median of 7 runs:
100 routes 203 -> 168 µs, 1000 routes 2044 -> 1680 µs (~18%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@carson-codeigniter4 carson-codeigniter4 Bot added 4.8 PRs that target the `4.8` branch. enhancement PRs that improve existing functionalities labels Oct 23, 2026

@michalsn michalsn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me - I also ran benchmarks locally and saw similar improvements.

Could we add a changelog entry under “Changes”? Something like this would work:

  • Router: Reduced route registration overhead in RouteCollection::create() by skipping placeholder replacement for route keys without : and calling helper('array') only once per collection.

@lonnieezell

Copy link
Copy Markdown
Member Author

Oh, yeah, completely forgot the change log. Incoming.

Comment thread user_guide_src/source/changelogs/v4.8.0.rst Outdated
Co-authored-by: Michal Sniatala <michal@sniatala.pl>
@lonnieezell
lonnieezell merged commit 50df18d into codeigniter4:4.8 Oct 24, 2026
61 checks passed
@lonnieezell
lonnieezell deleted the issue-10617 branch October 24, 2026 20:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4.8 PRs that target the `4.8` branch. enhancement PRs that improve existing functionalities

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants