Skip to content
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

Replace inline script for critical JS with standalone script #21429

Closed
wants to merge 4 commits into from

Conversation

silverwind
Copy link
Member

@silverwind silverwind commented Oct 12, 2022

The new script utilizes requestAnimationFrame to run DOM transformations as soon as possible. In my testing, this method is equivalent of a synchronous inline script in terms of outcome (no content flashes).

On Firefox, the callback sometimes fires too early and if that happens we register for another frame. I had 100% sucess within 2 frames in Firefox.

The new script utilizes requestAnimationFrame to run DOM transformations
as soon as possible. In my testing, this method is equivalent of a
synchronous inline script.

On Firefox, the callback sometimes fires too early and if that happens
we register for another frame. I had 100% sucess within 2 frames in
Firefox.
@silverwind silverwind added type/enhancement An improvement of existing functionality topic/ui Change the appearance of the Gitea UI labels Oct 12, 2022
@silverwind silverwind changed the title Replace inline script for cricital JS with standalone script Replace inline script for critictal JS with standalone script Oct 12, 2022
@silverwind silverwind changed the title Replace inline script for critictal JS with standalone script Replace inline script for critical JS with standalone script Oct 12, 2022
@wxiaoguang
Copy link
Contributor

wxiaoguang commented Oct 13, 2022

Although I approved the previous PRs, that's because of I think there are other valuable changes, but not because I consider using hacky methods and inline scripts to avoid flicker is the proper way.

Using hacky methods and inline scripts to avoid every flicker is the wrong way IMO. In the end, not every flicker can be avoided, because there could always be something rendered again after the page gets loaded. The hacky methods only make code harder to understand.

Take GitHub as an example, it also flicks on many pages. I can not see the value of using hacky methods to avoid every flicker. See GitHub pages:

image

image

And one more thing, instead of using these inline scripts, I recalled that we have discussed about that moving the Clone URLs into a dropdown menu, then there will be no flicker problem anymore.

@GiteaBot GiteaBot added the lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. label Oct 13, 2022
@silverwind
Copy link
Member Author

silverwind commented Oct 13, 2022

requestAnimationFrame is the proper way to mutate DOM without flicker. It can and should be avoided. It's just sloppiness on GitHub's side that they still have these flickers.

I will also move all stuff in formatting.js into this script as well. These are all critical replacements.

@silverwind silverwind marked this pull request as draft October 13, 2022 07:06
@wxiaoguang
Copy link
Contributor

wxiaoguang commented Oct 13, 2022

requestAnimationFrame is the proper way to mutate DOM without flicker.

Hmm, I am still not convinced that it's the proper way. Why attempts>100, not 50 or 1000?

It's just sloppiness on GitHub's side that they still have these flickers.

I wouldn't consider as they are sloppiness. Sometimes when the backend (tmpl) and frontend (like vue) rendered HTML are mixed together, it's very difficult (nearly impossible) to have a clear way to avoid all flickers.

@wxiaoguang
Copy link
Contributor

After reading the code of formatting.js, I have another question about "I will also move all stuff in formatting.js into this script as well. These are all critical replacements.":

Even if you move the formatting.js into the init.js, it still have problems. Imagine a case like this:

<script init.js>

<time id=time1>

<!-- 
very long content, which need a few seconds to transfer, 
then browser will partially render the content above,
while the remaining content will be rendered a few seconds later.
 -->

<time id=time2>

<script index.js>

How could you handle such case?

@silverwind
Copy link
Member Author

That will work as-is. It's going by the assumption that if <script index.js> is in the DOM, all preceding elements are in the DOM as well, which think is a rather safe choice. Could also change it to match on other stuff like <footer>, but it needs to be something that is present on every page.

@silverwind
Copy link
Member Author

silverwind commented Oct 13, 2022

Hmm, I am still not convinced that it's the proper way. Why attempts>100, not 50 or 1000?

That's just a last-resort way to run the init function in case we never find the script tag. Fast browsers usually complete in 1 attempt, but depending on connection speed, it might take a few couple of attempts to find the element in the DOM.

@wxiaoguang
Copy link
Contributor

That will work as-is. It's going by the assumption that if <script index.js> is in the DOM, all preceding elements are in the DOM as well, which think is a rather safe choice. Could also change it to match on other stuff like <footer>, but it needs to be something that is present on every page.

Hmm, I am still not convinced that it's the proper way. Why attempts>100, not 50 or 1000?

That's just a last-resort way to run the init function in case we never find the script tag. Fast browsers usually complete in 1 attempt, but depending on connection speed, it might take a few couple of attempts to find the element in the DOM.

How could it work properly? For example: if (document.querySelector('script[src*="index.js"]') || ++attempts > 100) return init();, if the page is partially render, does the requestAnimationFrame work? If requestAnimationFrame works, then the init() will run on a partially rendered page, then <time id=time2> won't be updated?

@silverwind
Copy link
Member Author

silverwind commented Oct 13, 2022

I think behaviour of requestAnimationFrame during page-load ist mostly undefined behaviour and browser-specific, but we do not need to concern ourselves with it. If the <script> tag is not found, the function will recurse into itself and request another frame (which IIRC, at earliest shoud happen a 60th of a second later), again looking for the script tag, and only if it's found, it will call init().

@silverwind
Copy link
Member Author

silverwind commented Oct 13, 2022

So assuming requestAnimationFrame callbacks happen at a 60th of a second, it might be wise to increase the maximum number of attempts. I initially had it based on the elapsed time (first callback argument), might be wise to use that again, and use something like 30s for the last-resort init() call.

@wxiaoguang
Copy link
Contributor

If the <script> tag is not found, the function will recurse into itself and request another frame (which IIRC, at earliest shoud happen a 60th of a second later), again looking for the script tag, and only if it's found, it will call init().

But the code reads like : if (hasIndexJs || attempts > 100) init()

Then back to the demo case: at the beginning, a page is partially rendered, then after 100th animation, init() is called, then the remaining page is rendered, then the newly rendered page's <time> won't be udpated?

@silverwind
Copy link
Member Author

silverwind commented Oct 13, 2022

Right, anything that happens after init will not be mutated. For that, a MutationObserver would be more fitting, and I'd certainly consider it for the <time> mutation, but at least for the clone states, I could not see a easy way to use a MutationObserver (as it affects many elements), so I went with this solution.

@wxiaoguang
Copy link
Contributor

wxiaoguang commented Oct 13, 2022

Right, anything that happens after init will not be mutated. , then how could this solution be proper ....

If a user's network is a little slow, they spends more than 1 seconds to fully load the page, then the init() won't take affect on newly rendered elements. They will see broken incorrect (not updated) page elements frequently?

@silverwind
Copy link
Member Author

silverwind commented Oct 13, 2022

Yes, the attempt counting is flawed, I will revert it to being time-based (e.g. before 577b5ff), so if a page does not load for example after 30s, we just execute init, even thought it will likely fail.

@wxiaoguang
Copy link
Contributor

wxiaoguang commented Oct 13, 2022

However, I am still not convinced that it's the proper way. The methods used are too hacky and do not seem to be easily tested or maintained.

Sometimes when the backend (tmpl) and frontend (like vue) rendered HTML are mixed together, it's very difficult (nearly impossible) to have a clear way to avoid all flickers.

@silverwind
Copy link
Member Author

It seems better than inline scripts which we should eliminate in an case because those are often forbidden via CSP.

@wxiaoguang
Copy link
Contributor

And one more thing, instead of using these inline scripts, I recalled that we have discussed about that moving the Clone URLs into a dropdown menu, then there will be no flicker problem anymore.

@silverwind
Copy link
Member Author

silverwind commented Oct 13, 2022

The final solution to clone states will be a backend user setting, than we can just render the right stuff into HTML, no JavaScript involved.

@wxiaoguang
Copy link
Contributor

wxiaoguang commented Oct 13, 2022

Yup, there could be better solutions than hacking the JS&DOM loading

@silverwind
Copy link
Member Author

Yeah, but the date formatting unfortunately has no server-side solution, so it will need this mutation mechanism even after clone states are properly fixed, so I consider this effort unavoidable :)

@wxiaoguang
Copy link
Contributor

wxiaoguang commented Oct 13, 2022

The only challenge for date/time formatting is timezone.

Timezone-based date formatting could also have a server-side solution, eg:

  • let users set their own timezone
  • use relative date/time instead of accurate ones
  • or even more: automatically save the browsers timezone to session and refresh the current page
    • when a user visits a page, render the session-saved timezone into the page, and use JS to check whether current session already has a timezone setting, if no, show a "preparing ..." shadow dialog (or just hide the whole HTML body) and update the timezone to backend by API/cookie, then reload current page.

Personally, I think it's better to tolerate the slight flicker for timezone-based date formatting by JS instead of making the problem too complex.

@silverwind
Copy link
Member Author

Personally, I can't tolerate flicker/layout shift during load, it's a sign of low-quality to me.

@wxiaoguang
Copy link
Contributor

wxiaoguang commented Oct 13, 2022

I can make a flawless (nearly 😁 ) solution for "or even more: automatically save the browsers timezone to session and refresh the current page ", if you do not mind.

Then the backend can get the browser's locale and timezone, and users can get consistent rendered result by backend and by frontend. It could be the best solution for Gitea at the moment because Gitea's frontend mixes a lot of things together.

@wxiaoguang
Copy link
Contributor

lunny pushed a commit that referenced this pull request Feb 17, 2023
…mplates (#22861)

This PR follows:
* #21986
* #22831

This PR also introduce customized HTML elements, which would also help
problems like:
* #17760
* #21429
* #21440

With customized HTML elements, there won't be any load-search-replace
operations, and it can avoid page flicking (which @silverwind cares a
lot).

Browser support:
https://developer.mozilla.org/en-US/docs/Web/API/Window/customElements

# FAQ

## Why the component has the prefix?

As usual, I would strongly suggest to add prefixes for our own/private
names. The dedicated prefix will avoid conflicts in the future, and it
makes it easier to introduce various 3rd components, like GitHub's
`relative-time` component. If there is no prefix, it's impossible to
introduce another public component with the same name in the future.

## Why the `custcomp.js` is loaded before HTML body? The `index.js` is
after HTML body.

Customized components must be registered before the content loading.
Otherwise there would be still some flicking.

`custcomp.js` should have its own dependencies and should be very light,
so it won't affect the page loading time too much.

## Why use `data-url` attribute but not use the `textContent`?

According to the standard, the `connectedCallback` occurs on the
tag-opening moment. The element's children are not ready yet.

## Why not use `{{.GuessCurrentOrigin $.ctx ...}}` to let backend decide
the absolute URL?

It's difficult for backend to guess the correct protocol(scheme)
correctly with zero configuration. Generating the absolute URL from
frontend can guarantee that the URL is 100% correct -- since the user is
visiting it.

# Screenshot

<details>

![image](https://user-images.githubusercontent.com/2114189/218256757-a267c8ba-3108-4755-9ae5-329f1b08f615.png)

</details>
@silverwind
Copy link
Member Author

silverwind commented Apr 1, 2023

Would still like to eventually have a script like this as not all things can be done in webcomponents and I'd like to remove all the inline scripts eventually. Maybe there is a solution without requestAnimationFrame.

As for this PR, it's no longer needed.

@silverwind silverwind closed this Apr 1, 2023
@wxiaoguang
Copy link
Contributor

I will try to refactor the number and time JS to Custom Elements.

The "Clone" popup could be resolved by #23231

@silverwind
Copy link
Member Author

See if you can use https://github.com/github/relative-time-element for time.

I was referring to the recent inline JS added in #23553. Those should really be in .js files instead.

@wxiaoguang
Copy link
Contributor

PrettyNumber => LocaleNumber: #23861

jolheiser pushed a commit that referenced this pull request Apr 3, 2023
…ion on pages. (#23861)

Follow #21429 & #22861

Use `<gitea-locale-number>` instead of backend `PrettyNumber`. All old
`PrettyNumber` related functions are removed. A lot of code could be
simplified.

And some functions haven't been used for long time (dead code), so they
are also removed by the way (eg: `SplitStringAtRuneN`, `Dedent`)

This PR only tries to improve the `PrettyNumber` rendering problem, it
doesn't touch the "plural" problem.

Screenshot:


![image](https://user-images.githubusercontent.com/2114189/229290804-1f63db65-1e34-4a54-84ba-e00b44331b17.png)


![image](https://user-images.githubusercontent.com/2114189/229290911-c88dea00-b11d-48dd-accb-9f52edd73ce4.png)
@go-gitea go-gitea locked and limited conversation to collaborators May 3, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. topic/ui Change the appearance of the Gitea UI type/enhancement An improvement of existing functionality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants