possible performance reengineering
hi, WebCoder...
I spent some time tuning my highlighter (it's really fast now, a single regexp tokenizes the full text, generating the minimum number of nodes etc.) . Even so, handling typical files with around 600 lines (it's a good upper bound for me), CodeInput was not satisfactorily responsive. I've been wondering if I should implement highlighting only the visible scroll window at each input event, scheduling the full highlight with window.requestIdleCallback(). But I decided to try a hack first... and it seems to be giving satisfactory results by now...
The hack is to perform a kind of rendering throttling. This way:
// request redraw
el.must_redraw = true;
window.requestAnimationFrame(() => {
if (el.must_redraw) {
el.must_redraw = false;
ulight_redraw(el, lang);
} /*else {
console.log('skipped');
} */
});
When keystrokes are too fast, requests are queued; but instead of executing them FIFO, we jump to the current version of the text, ignoring the steps along the way.
This is a delicate change, so I didn't change your update() function, asking for a pull request.
When you have time, take a look, do some tests... Tell us what you think...
best regards andrelsm
dear WebCoder...
I decided to write a similar, simplified and more responsive TextEditor class based on CodeInput main idea. I would like to share with you the basis of the thing, which is performing very well. In hopes that it might inspire a little re-engineering of codeInput, if you and the maintainer group deem it worthwhile.
It's worth studying window.requestAnimationFrame(), understanding the advantages of putting updates there.
The few lines below provide basic editing functionality.
class TextEditor {
#container;
#highlighter;
#textarea;
#lang;
#must_redraw;
#update(must = false) {
this.#highlighter.scrollTo(this.#textarea.scrollLeft, this.#textarea.scrollTop);
if (must || this.#must_redraw) {
this.#must_redraw = false;
// use your highlighter...
ulight.apply(this.#highlighter, this.#lang, this.#textarea.value);
}
window.requestAnimationFrame(() => {
this.#update();
});
}
get value() {
return this.#textarea.value;
}
set value(text) {
this.#textarea.value = text;
this.#update(true);
}
constructor(e, lang) {
this.#container = e.appendChild(document.createElement('div'));
this.#highlighter = this.#container.appendChild(document.createElement('div'));
this.#textarea = this.#container.appendChild(document.createElement('textarea'));
this.#lang = lang;
this.#textarea.spellcheck = false;
this.#container.classList.add('texteditor');
this.#highlighter.classList.add('ulight');
this.#textarea.addEventListener('input', () => {
this.#must_redraw = true;
});
}
}
I apologize for my coding style, I'm an old school programmer...
my best regards andrelsm
Thank you so much! Improving performance should definitely be focused on more here in code-input than it has been in the past. Thank you again for all your time and work with this project, and please let me know if you have any more suggestions on how to improve code-input. I can see that responsiveness was one large factor in your desire to reduce complexity - were there any others?
Another aspect to improve performance in:
The reason why CodeMirror, Ace, Monaco, and the like can work faster even though they have bigger source code is because they work on the stream. Only parts that are in the view will be highlighted, any text below and above the scroll bar will be ignored. (Taufik Nurrohman, Apr 2021)
I believe this will be difficult to implement with HLJS/Prism.js but is quite important - if the mainstream highlighters don't support it it should still be easy to implement with custom highlighting templates.
Surely a definitive solution, with a custom highlighter, would be to generate highlight nodes only for the visible window (scroll window).
The suggestion I made in this code works well for a generic highlighter and brings good gains in performance and simplicity. It's serving me well for now.
When you have some time, take a look at this code, do some tests, see if it's worth using this idea of the update happening in a window.requestAnimationFrame() callback.
PS: I managed to have plugins (registered in classe prototype). crtl-G / indent are working fine.
[ ] andrelsm
I've started simplifying the codebase using requestAnimationFrame and have moved the scrolling synchronisation to be CSS' responsibility, and it appears to be working well.
My solution includes basically this wrapper to greatly boost its performance...
var updateBurst=0;
function updateCode() {
if(updateBurst==1)return;
updateBurst=1;
setTimeout(()=>{
updateBurst=0;
..... regular code is here.....
},1);
};
For larger scripts the editor still runs stable even if i type fast (e.g. holding space/enter/tab). It may delay the display of a single or even two characters, but it will not cause the editor to get completely stucked because of multiple parallel code updates and prism renderings.
Cheers, Kai
@KaiWilke Basically the same idea of a "rendering throttling". But: requestAnimationFrame() callbacks are the ideal place to all DOM updates, because no engine rendering happen meanwhile; nor user interaction events (such callbacks are called pre-paint). With a few lines more than my previous example, for attaching plugins registered in TextEditor.prototype.plugins... I have a full functional editor which performs pretty well. So I shared the idea....
I've started simplifying the codebase using requestAnimationFrame and have moved the scrolling synchronisation to be CSS' responsibility, and it appears to be working well.
great!
Using bounding boxes, editing elements inside the result element directly, and only highlighting some of the top-level tokens chosen by what is visible rather than all of them on every keystroke, I think it will be relatively easy to make the Prism/highlight.js templates for the library much faster (optionally activated) - what do you think?
the main idea behind CodeInput remains the same, right?
I really liked the idea of mirroring a textarea. It allows a class with as little code as TextEditor (above) to offer basic editing (and with a few more lines, plugins). And performing well... Or "not so bad", at least.
Using generic highlighters as Prism/highlight.js is a very nice fetaure, too.
Highlighting is context-dependent. If, on the one hand, it is only affected by the text starting from the cursor, on the other hand, it depends on the previous context. For example, you can have the beginning of a multi-line comment on the first line above the visible region. Keeping with the current CodeInput structure, you could perhaps ask highlighting of [1, l] lines (l is the last visible line in the scroll window -- not difficult to determine from the scrollTop, clientHeight and lineHeight). And, in a post-highlighting step, replace all nodes in [1, f-1] (f is the first line visible in the scroll window) by "\n" textNodes. And request a window.requestIdleCallback() for full highlighting. (That way no highlighting while scrolling will be needed). But... I really don't know if all this hocus pocus will perform better than just drawing the entire text... Need to test and measure...
Did I understand your ideas correctly?
regards andrelsm
@andrelsm You could also simplify partial renderings by empty the currently hidden lines while copying the code from edit to display
But as you pointed out, the context may become lost if you highlight only those currently visible lines, without using a language specific detection where partially highlighting is supported and where not.
BTW: I performance optimized the prism TCL language definition (this is the language used in my solution). I dont know how great HTML/JScript language definitions are . But for TCL i was able to speed up renderings of TCL scripts by a factor of 4 while also improving the detection rate. Watch closely to the normalized RegEx syntax, its has become a huge decision tree...
https://github.com/KaiWilke/F5-PrismJS-iRule-Language-Definition/blob/main/iRuleLanguage.js
Cheers, Kai
@andrelsm the main concept will definitely remain the same - additionally I believe that in Prism.js at least the top-level tokens are free of context so can be highlighted like a normal code block. The yellow token below is a top-level token. By detecting how the selected region in the textarea changes and then highlighting only certain top-level tokens I will add partial highlighting then test whether it is faster than the old solution.
looking forward for the tests results...
If you will write a custom highlighter based on Prism... Maybe you can store parsing states in the pre nodes... So you could start parsing really from the visible top line to the visible bottom line.
good luck... regards andrelsm
I have made a sort-of-highlighter-independent method for partial highlighting which is in the partial-highlighting-optimisation branch and works with both Prism.js and highlight.js. However, it does not display significant advantages over your optimisations and still has a few bugs so I will treat it as a work-in-progress for now.
I aim to get the main performance-improvements released soon, once I have found a Firefox-compatible Ctrl+F fix.
dear friend, good morning,
Thank you so much for your efforts.
After thinking a little about the topic, I come to the following conclusions:
-
a selective highlight should be the responsibility of the highlighter, there is probably no agnostic means of doing this efficiently.
-
CodeInput is a lightweight solution in principle, allowing the choice of the user's preferred highlighter (or even a customized one). I think general optimization is important and welcome, but it might be better to assume that it has performance limitations for large files. Projects that need to handle large files should rely on other already established solutions, such as the Chrome AceArea addon (2.2Mbytes footprint) etc.
my very best regards [ ] andrelsm
This may be more interesting now in a subclass of codeInput.Template, especially given with the ECMAScript Module structure templates are becoming more separate from the main file (probably easiest when source moves to ESM). At least try the debouncing mentioned in the first comment here.