Repository: browserslist/browserslist (language: javascript)

Issue:
opts.path interferes with resolved query caching
When debugging our build, I've noticed that even after forcing `BROWSERSLIST_CONFIG` as a workaround for #859 , browserslist still takes ~1s to run. About half that time is spent on [repeatedly resolving](https://github.com/browserslist/browserslist/blob/0639f5484b46b6c8608eab13a22310e9922feb96/index.js#L424) our single browserslist config into browsers:

<img width="1053" alt="Снимок экрана 2024-12-14 в 17 28 08" src="https://github.com/user-attachments/assets/53a56bf1-9c30-4c66-9bed-696f9eca979e" />

From the looks of it, the resolution cache key includes `opts.path`, which, in our case (vite -> autoprefixer -> browserslist) equals CSS file path:

https://github.com/browserslist/browserslist/blob/0639f5484b46b6c8608eab13a22310e9922feb96/index.js#L404C1-L421C52

However, nothing in `resolve` seems to reference `path` (or any `context` field other than `mobileToDesktop`, for that matter. 

Could we maybe exclude `path` from `cacheKey`? Together with the #859 workaround, this brings browserslist time to a reasonable 400ms:

<img width="1094" alt="Снимок экрана 2024-12-14 в 17 35 09" src="https://github.com/user-attachments/assets/53cccfd4-07fc-4a75-9291-dd903a74ddb8" />


Discussion / hints:
> However, nothing in resolve seems to reference path (or any context field other than mobileToDesktop, for that matter.

No. Browserslist is looking for Browserslist config closest to the file.

https://github.com/browserslist/browserslist/blob/main/node.js#L258-L259

The original idea is to allow having different browsers depending on file path:

```
src/
  legacy/
    .browserslistrc
    index.css // Legacy browsers
  index.css // Modern browsers
.browserslistrc
```

Maybe we can change it in next major, but not before it.

Maybe we can add env var to disable realtive path search?
OK, I've overlooked a few things, but still — locating nearest browserslist config happens before this particular cache:

https://github.com/browserslist/browserslist/blob/0639f5484b46b6c8608eab13a22310e9922feb96/index.js#L401

So the main cache in only caches this logic:

https://github.com/browserslist/browserslist/blob/0639f5484b46b6c8608eab13a22310e9922feb96/index.js#L424-L437
  
If we follow `context`, it's passed to [resolve,](https://github.com/browserslist/browserslist/blob/0639f5484b46b6c8608eab13a22310e9922feb96/index.js#L321), which forwards it to `QUERIES[node.type].select`, and `select` [implementations](https://github.com/browserslist/browserslist/blob/0639f5484b46b6c8608eab13a22310e9922feb96/index.js#L601) mostly pass it to 
- [byName](https://github.com/browserslist/browserslist/blob/0639f5484b46b6c8608eab13a22310e9922feb96/index.js#L234) — only reads `mobileToDesktop`
- [filterJumps](https://github.com/browserslist/browserslist/blob/0639f5484b46b6c8608eab13a22310e9922feb96/index.js#L293) — only reads `mobileToDesktop`
- [checkName](https://github.com/browserslist/browserslist/blob/0639f5484b46b6c8608eab13a22310e9922feb96/index.js#L276) — calls byName
- [filterByYear](https://github.com/browserslist/browserslist/blob/0639f5484b46b6c8608eab13a22310e9922feb96/index.js#L209) — calls byName

Some queries do access a few more fields, but not `path`:
- `popularity_in_my_stats, popularity_in_config_stats, cover, cover_in` — read `currentUsage`
- `node_major_version, node_minor_version, node_patch_version, browser_version `— read `ignoreUnknownVersions`

`maintained_node`, `defaults`, `dead` — call `resolve` recursively, which does not change much.

This leaves us with 2 exotic queries that do, in fact, depend on path:
- [browserslist_config](https://github.com/browserslist/browserslist/blob/0639f5484b46b6c8608eab13a22310e9922feb96/index.js#L1133), which recurses to `browserslist`
- [extends](https://github.com/browserslist/browserslist/blob/0639f5484b46b6c8608eab13a22310e9922feb96/index.js#L1140) which calls [loadQueries](https://github.com/browserslist/browserslist/blob/0639f5484b46b6c8608eab13a22310e9922feb96/node.js#L198)

Maybe we could eagerly check if these particular queries are in the query set, and omit `path` from `cacheKey` if they're not present?
Cache is using here https://github.com/browserslist/browserslist/blob/main/node.js#L392-L394
> Cache is using here https://github.com/browserslist/browserslist/blob/main/node.js#L392-L394

browserslist has multile cache layers, in this issue let's focus on the one in `index.js`

https://github.com/browserslist/browserslist/blob/0639f5484b46b6c8608eab13a22310e9922feb96/index.js#L421-L422

it caches query resolution, e.g. 

```js
queries = ["last 2 years AND >.05%","ios >= 13","not IE 11","not op_mini all","not dead"]
context = {"path":"/Users/vklepov/project/src/cryptopro/+page.svelte.vite-preprocess.postcss"}
```

into

```js
['and_chr 129', 'and_ff 130', 'and_qq 14.9', 'and_uc 15.5', 'android 129', 'chrome 128', ...etc]
```

On this layer, including `path` in `cacheKey` makes the cache largely non-functional, because each CSS file generates a unique cache key.
Here's a PR #862 to better illustrate the idea

Fix patch:
diff --git a/index.js b/index.js
index 3df783c0..d995d16e 100644
--- a/index.js
+++ b/index.js
@@ -6,7 +6,7 @@ var path = require('path')
 
 var BrowserslistError = require('./error')
 var env = require('./node')
-var parse = require('./parse') // Will load browser.js in webpack
+var parseWithoutCache = require('./parse') // Will load browser.js in webpack
 
 var YEAR = 365.259641 * 24 * 60 * 60 * 1000
 var ANDROID_EVERGREEN_FIRST = '37'
@@ -319,7 +319,7 @@ function isSupported(flags, withPartial) {
 }
 
 function resolve(queries, context) {
-  return parse(QUERIES, queries).reduce(function (result, node, index) {
+  return parseQueries(queries).reduce(function (result, node, index) {
     if (node.not && index === 0) {
       throw new BrowserslistError(
         'Write any browsers query (for instance, `defaults`) ' +
@@ -395,19 +395,26 @@ function checkQueries(queries) {
 }
 
 var cache = {}
+var parseCache = {}
 
 function browserslist(queries, opts) {
   opts = prepareOpts(opts)
   queries = prepareQueries(queries, opts)
   checkQueries(queries)
 
+  var needsPath = parseQueries(queries).some(function (node) {
+    return QUERIES[node.type].needsPath
+  })
   var context = {
     ignoreUnknownVersions: opts.ignoreUnknownVersions,
     dangerousExtend: opts.dangerousExtend,
     mobileToDesktop: opts.mobileToDesktop,
-    path: opts.path,
     env: opts.env
   }
+  // Removing to avoid using context.path without marking query as needsPath
+  if (needsPath) {
+    context.path = opts.path
+  }
 
   env.oldDataWarning(browserslist.data)
   var stats = env.getStat(opts, browserslist.data)
@@ -441,11 +448,21 @@ function browserslist(queries, opts) {
   return result
 }
 
+function parseQueries(queries) {
+  var cacheKey = JSON.stringify(queries)
+  if (cacheKey in parseCache) return parseCache[cacheKey]
+  var result = parseWithoutCache(QUERIES, queries)
+  if (!env.env.BROWSERSLIST_DISABLE_CACHE) {
+    parseCache[cacheKey] = result
+  }
+  return result
+}
+
 browserslist.parse = function (queries, opts) {
   opts = prepareOpts(opts)
   queries = prepareQueries(queries, opts)
   checkQueries(queries)
-  return parse(QUERIES, queries)
+  return parseQueries(queries)
 }
 
 // Will be filled by Can I Use data below
@@ -1133,6 +1150,7 @@ var QUERIES = {
   browserslist_config: {
     matches: [],
     regexp: /^browserslist config$/i,
+    needsPath: true,
     select: function (context) {
       return browserslist(undefined, context)
     }
@@ -1140,6 +1158,7 @@ var QUERIES = {
   extends: {
     matches: ['config'],
     regexp: /^extends (.+)$/i,
+    needsPath: true,
     select: function (context, node) {
       return resolve(env.loadQueries(context, node.config), context)
     }


Test patch: