-
-
Notifications
You must be signed in to change notification settings - Fork 116
♻️ (blaze/attrs): optimize attribute updates by caching last values #478
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
…o avoid redundant DOM updates and ensure handlers are cleaned up properly
…getUrlProtocol Add standalone HTML files to benchmark and compare performance of ElementAttributesUpdater (_lastValues cache) and getUrlProtocol (with and without protocol cache). These benchmarks help quantify the performance impact of recent optimizations and provide reproducible evidence for future improvements.
jankapunkt
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for this improvement! I will thoroughly test this using BlazeUI once we have a alpha release for this, because in BlazeUI I am defining components only around attributes so I can easily tell you if things are breaking.
As I commented, please add more inline docs (Blaze is already missing inline docs a lot, so we need a higher comment to code ratio).
One concern that is left is, whether tests deeply enough covered that caching won't break reactivity and various contexts (with, async, each, helpers etc.) where attributes can play a role.
Expanded and clarified documentation for all attribute handler classes in packages/blaze/attrs.js. Added JSDoc comments, usage examples, and security notes for each handler type, improving maintainability and developer understanding. No functional changes were made.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements performance optimizations for Blaze's attribute system by introducing caching mechanisms to reduce redundant DOM operations. The main focus is optimizing attribute updates by tracking the last known values and only performing DOM updates when values actually change.
- Adds URL protocol caching with LRU eviction to avoid repeated DOM operations for URL validation
- Implements
_lastValuescache in ElementAttributesUpdater to prevent redundant attribute updates - Updates test application dependencies to Meteor 3.3-beta.1 and enables modern bundle mode
Reviewed Changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
packages/blaze/attrs.js |
Core optimization implementation with protocol caching and last-values tracking |
tests/benchmark_getUrlProtocol.html |
Benchmark suite for measuring URL protocol detection performance improvements |
tests/benchmark_ElementAttributesUpdater.html |
Benchmark suite for measuring attribute update performance improvements |
test-app/package.json |
Enables modern bundle mode for the test application |
test-app/.meteor/versions |
Updates Meteor packages to 3.3-beta.1 versions |
test-app/.meteor/release |
Updates Meteor version to 3.3-beta.1 |
test-app/.meteor/packages |
Updates package version references for compatibility |
Comments suppressed due to low confidence (2)
packages/blaze/attrs.js:139
- [nitpick] The shouldUpdate logic is complex and spans multiple lines. Consider extracting this into a helper function like
hasValueChanged(last, value)to improve readability and testability.
const currentAttrsMap = currentAttrString ? this.parseValue(currentAttrString) : new OrderedDict();
tests/benchmark_getUrlProtocol.html:174
- Using Date.now() for generating unique IDs could cause collisions if multiple elements are created in rapid succession. Consider using a counter or more robust unique ID generation.
console.log(`Average Cached Time: ${avgCachedTime.toFixed(3)} ms`);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (handler) { // Proceed only if a handler exists or was created | ||
| const lastCachedValue = lastValues[k]; // Read from cache | ||
| // The crucial check using _lastValues | ||
| const shouldUpdate = (lastCachedValue !== newValueForAttr && String(lastCachedValue) !== String(newValueForAttr)); // Changed || to && |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic in the benchmark doesn't match the actual implementation in attrs.js. The benchmark uses a simple comparison (lastCachedValue !== newValueForAttr && String(lastCachedValue) !== String(newValueForAttr)) on line 138, but the actual implementation in attrs.js uses a more complex condition with additional null handling logic on lines 736-738. This inconsistency means the benchmark isn't accurately testing the actual code behavior.
| const shouldUpdate = (lastCachedValue !== newValueForAttr && String(lastCachedValue) !== String(newValueForAttr)); // Changed || to && | |
| // Match the null-handling logic from attrs.js | |
| const shouldUpdate = ( | |
| lastCachedValue !== newValueForAttr && | |
| !(lastCachedValue == null && newValueForAttr == null) && | |
| String(lastCachedValue) !== String(newValueForAttr) | |
| ); |
| // Performance optimization: only update if value actually changed | ||
| // This handles edge cases with null/undefined values and type coercion | ||
| const last = lastValues[k]; | ||
| const shouldUpdate = last !== value && | ||
| !((last == null && value == null) || | ||
| (typeof last === typeof value && String(last) === String(value))); |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The shouldUpdate logic has a flaw when handling new attributes. When an attribute is first created (line 724), lastValues[k] is undefined. The condition on lines 736-738 evaluates:
last !== value→undefined !== value(true for non-undefined values)(last == null && value == null)→ true only if value is also null/undefinedString(last) === String(value)→"undefined" === String(value)
This means a newly created attribute with a non-null value will correctly trigger an update. However, if value is the literal string "undefined", the String comparison will be true, causing the update to be skipped incorrectly. Additionally, the logic doesn't initialize lastValues[k] when the handler is created, which means subsequent calls will continue treating it as undefined until the first update occurs.
| const shouldUpdate = last !== value && | ||
| !((last == null && value == null) || |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The shouldUpdate condition performs a String() conversion on every check (line 738), which has a performance cost. For attributes that update frequently, this string conversion happens on every render cycle even when values haven't changed. This partially defeats the purpose of the optimization. Consider checking strict equality first, and only falling back to string comparison if that fails, or moving the string comparison to a separate branch to avoid unnecessary conversions.
| const shouldUpdate = last !== value && | |
| !((last == null && value == null) || | |
| const shouldUpdate = !(last === value || | |
| (last == null && value == null) || |
| return (anchorForNormalization.protocol || "").toLowerCase(); | ||
| const protocol = (anchorForNormalization.protocol || "").toLowerCase(); | ||
|
|
||
| // Implement simple LRU cache eviction to prevent memory leaks |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment on line 526 says "Implement simple LRU cache eviction" but the code actually implements FIFO (First In, First Out) eviction, not LRU (Least Recently Used). LRU would require updating an item's position when it's accessed via get(), which this implementation doesn't do. The comment should be updated to reflect the actual behavior.
| // Implement simple LRU cache eviction to prevent memory leaks | |
| // Implement simple FIFO cache eviction to prevent memory leaks |
| function runBenchmarkIteration(UpdaterConstructor, description, statusElement, suppressConsoleLog = false) { | ||
| const testElement = document.createElement('div'); | ||
| // Assign unique ID for potential debugging, though not strictly needed for logic | ||
| testElement.id = description.replace(/\s+/g, '') + "_" + Date.now(); // Corrected regex replacement string |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment states "Corrected regex replacement string" but there's no indication of what was corrected or why this comment is necessary. The code description.replace(/\s+/g, '') is standard JavaScript for removing all whitespace, and there's no apparent correction. This comment should either be removed or clarified to explain what issue was corrected.
| testElement.id = description.replace(/\s+/g, '') + "_" + Date.now(); // Corrected regex replacement string | |
| testElement.id = description.replace(/\s+/g, '') + "_" + Date.now(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment states "Corrected regex replacement string" but there's no indication of what was corrected or why this comment is necessary
| const getUrlProtocol = function (url) { | ||
| // Check cache first for performance | ||
| if (_protocolCache.has(url)) { | ||
| return _protocolCache.get(url); | ||
| } | ||
|
|
||
| if (Meteor.isClient) { | ||
| // Use browser's URL parsing via anchor element | ||
| anchorForNormalization.href = url; | ||
| return (anchorForNormalization.protocol || "").toLowerCase(); | ||
| const protocol = (anchorForNormalization.protocol || "").toLowerCase(); | ||
|
|
||
| // Implement simple LRU cache eviction to prevent memory leaks | ||
| if (_protocolCache.size >= MAX_CACHE_SIZE) { | ||
| // Remove oldest entry (first key in insertion order) | ||
| const firstKey = _protocolCache.keys().next().value; | ||
| _protocolCache.delete(firstKey); | ||
| } | ||
|
|
||
| // Cache the result for future use | ||
| _protocolCache.set(url, protocol); | ||
| return protocol; | ||
| } else { | ||
| throw new Error('getUrlProtocol not implemented on the server'); | ||
| } | ||
| }; |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The getUrlProtocol function doesn't validate the input parameter url. If url is not a string (e.g., null, undefined, number, object), setting it as anchorForNormalization.href could cause unexpected behavior or errors. While JavaScript will coerce these values to strings, this could lead to incorrect cache entries and protocol detection. For example, null becomes the string "null", and undefined becomes "undefined", which would be parsed as relative URLs and could produce unexpected protocols.
| /** | ||
| * Protocol cache for performance optimization | ||
| * | ||
| * Since URL protocol detection requires DOM manipulation, we cache results | ||
| * to avoid repeated operations on the same URLs. Uses LRU-style eviction | ||
| * to prevent memory leaks in long-running applications. |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation states "Uses LRU-style eviction to prevent memory leaks" but the implementation is not actually LRU (Least Recently Used). The code removes the oldest entry by insertion order (FIFO - First In, First Out), not the least recently accessed entry. In true LRU, accessing an existing cached value would move it to the end of the queue, but this implementation doesn't do that - it only checks has() and returns the value without updating its position. The documentation should be corrected to say "Uses FIFO eviction" or the implementation should be updated to properly implement LRU.
| const _protocolCache = new Map(); | ||
| const MAX_CACHE_SIZE = 1000; | ||
|
|
||
| /** |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The protocol cache is shared globally across all ElementAttributesUpdater instances and persists for the lifetime of the application. If a URL's protocol changes during the application lifecycle (which is theoretically possible if the page's base URL changes or if relative URLs are used), the cached protocol would be incorrect. Additionally, there's no mechanism to clear or reset the cache, which could be problematic for testing or specific use cases. Consider adding a cache clearing function or documenting this limitation.
| /** | |
| /** | |
| * Clear the protocol cache. | |
| * | |
| * This is useful for testing or in cases where the base URL or URL resolution context changes | |
| * during the application lifecycle. Use with caution in production. | |
| */ | |
| Blaze._clearProtocolCache = function () { | |
| _protocolCache.clear(); | |
| }; | |
| /** |
italojs
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, we have a unecessary comment but I'll aprove it
| function runBenchmarkIteration(UpdaterConstructor, description, statusElement, suppressConsoleLog = false) { | ||
| const testElement = document.createElement('div'); | ||
| // Assign unique ID for potential debugging, though not strictly needed for logic | ||
| testElement.id = description.replace(/\s+/g, '') + "_" + Date.now(); // Corrected regex replacement string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment states "Corrected regex replacement string" but there's no indication of what was corrected or why this comment is necessary
italojs
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry, never mind the last approval. I didn't see the copilot review and need to take a better look.
BTW: I got surprised with that AI Review
| if (handler) { // Proceed only if a handler exists or was created | ||
| const lastCachedValue = lastValues[k]; // Read from cache | ||
| // The crucial check using _lastValues | ||
| const shouldUpdate = (lastCachedValue !== newValueForAttr && String(lastCachedValue) !== String(newValueForAttr)); // Changed || to && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you remove this comment, please?
|
CI now reports While processing files with ecmascript (for target os.linux.x86_64):
packages/babel-compiler.js:126:18: Plugin?.getMeteorConfig is not a
function
at getMeteorConfig (packages/babel-compiler.js:126:18)
at BabelCompiler.BCp.initializeMeteorAppConfig
(packages/babel-compiler.js:189:24)
at BabelCompiler.BCp.processFilesForTarget
(packages/babel-compiler.js:294:8)
While building package templating-runtime:
error: No plugin known to handle file 'dynamic.html'. If you want this file
to be a static asset, use addAssets instead of addFiles; eg,
api.addAssets('dynamic.html', 'client').
While building package local-test:templating-runtime:
error: No plugin known to handle file 'dynamic_tests.html'. If you want this
file to be a static asset, use addAssets instead of addFiles; eg,
api.addAssets('dynamic_tests.html', 'client').@wreiske does this occur on your local tests as well? |

This is an experimental PR for some performance improvements.
22e0df1
6d849c8