64 votes

JQuery UI Autocomplete Combobox Très Lent Avec de Grandes Listes de Sélection

Je utilise une version modifiée de l'Autocomplete Combobox de jQuery UI, comme on peut le voir ici : http://jqueryui.com/demos/autocomplete/#combobox

Pour les besoins de cette question, disons que j'ai exactement ce code ^^^

Lors de l'ouverture de la combobox, que ce soit en cliquant sur le bouton ou en se concentrant sur l'entrée de texte de la combobox, il y a un grand retard avant d'afficher la liste des éléments. Ce retard devient nettement plus important lorsque la liste de sélection a plus d'options.

Ce retard ne se produit pas simplement la première fois non plus, il se produit à chaque fois.

Comme certaines des listes de sélection de ce projet sont très grandes (des centaines et des centaines d'éléments), le retard/le gel du navigateur est inacceptable.

Est-ce que quelqu'un peut me guider dans la bonne direction pour optimiser cela ? Ou même où le problème de performance pourrait être ?

Je crois que le problème pourrait être lié à la façon dont le script affiche la liste complète des éléments (effectue une recherche de saisie semi-automatique pour une chaîne vide), y a-t-il une autre façon d'afficher tous les éléments ? Peut-être pourrais-je créer un cas unique pour afficher tous les éléements (car il est courant d'ouvrir la liste avant de commencer à taper) qui ne fait pas toute la correspondance regex ?

Voici un jsfiddle pour y travailler : http://jsfiddle.net/9TaMu/

0 votes

Vous verriez probablement les plus grandes augmentations de vitesse en effectuant toutes les opérations de regex et de manipulation avant la création du widget, de sorte que seuls des recherches simples dans les tableaux/objets soient effectuées lorsque le widget est utilisé.

79voto

gary Points 1116

Avec l'actuelle zone de liste déroulante mise en œuvre, la liste complète est vidé et un nouveau rendu à chaque fois que vous développez la liste déroulante. Aussi, vous êtes coincé avec le réglage de la minLength à 0, car il a à faire une recherche vide pour obtenir la liste complète.

Voici ma propre mise en œuvre de l'extension de la saisie semi-automatique widget. Dans mes tests, il peut gérer des listes de 5000 articles assez en douceur, même sur IE 7 et 8. Il rend la liste complète juste une fois, et le réutilise à chaque fois que le bouton de la liste déroulante est cliqué. Cela supprime également la dépendance de l'option minLength = 0. Il travaille aussi avec des tableaux, et ajax comme source de liste. Aussi, si vous avez plusieurs de grandes de la liste, le widget d'initialisation est ajouté à une file d'attente de sorte qu'il peut fonctionner en arrière-plan, et de ne pas figer le navigateur.

<script>
(function($){
    $.widget( "ui.combobox", $.ui.autocomplete, 
        {
        options: { 
            /* override default values here */
            minLength: 2,
            /* the argument to pass to ajax to get the complete list */
            ajaxGetAll: {get: "all"}
        },

        _create: function(){
            if (this.element.is("SELECT")){
                this._selectInit();
                return;
            }

            $.ui.autocomplete.prototype._create.call(this);
            var input = this.element;
            input.addClass( "ui-widget ui-widget-content ui-corner-left" );

            this.button = $( "<button type='button'>&nbsp;</button>" )
            .attr( "tabIndex", -1 )
            .attr( "title", "Show All Items" )
            .insertAfter( input )
            .button({
                icons: { primary: "ui-icon-triangle-1-s" },
                text: false
            })
            .removeClass( "ui-corner-all" )
            .addClass( "ui-corner-right ui-button-icon" )
            .click(function(event) {
                // close if already visible
                if ( input.combobox( "widget" ).is( ":visible" ) ) {
                    input.combobox( "close" );
                    return;
                }
                // when user clicks the show all button, we display the cached full menu
                var data = input.data("combobox");
                clearTimeout( data.closing );
                if (!input.isFullMenu){
                    data._swapMenu();
                    input.isFullMenu = true;
                }
                /* input/select that are initially hidden (display=none, i.e. second level menus), 
                   will not have position cordinates until they are visible. */
                input.combobox( "widget" ).css( "display", "block" )
                .position($.extend({ of: input },
                    data.options.position
                    ));
                input.focus();
                data._trigger( "open" );
            });

            /* to better handle large lists, put in a queue and process sequentially */
            $(document).queue(function(){
                var data = input.data("combobox");
                if ($.isArray(data.options.source)){ 
                    $.ui.combobox.prototype._renderFullMenu.call(data, data.options.source);
                }else if (typeof data.options.source === "string") {
                    $.getJSON(data.options.source, data.options.ajaxGetAll , function(source){
                        $.ui.combobox.prototype._renderFullMenu.call(data, source);
                    });
                }else {
                    $.ui.combobox.prototype._renderFullMenu.call(data, data.source());
                }
            });
        },

        /* initialize the full list of items, this menu will be reused whenever the user clicks the show all button */
        _renderFullMenu: function(source){
            var self = this,
                input = this.element,
                ul = input.data( "combobox" ).menu.element,
                lis = [];
            source = this._normalize(source); 
            input.data( "combobox" ).menuAll = input.data( "combobox" ).menu.element.clone(true).appendTo("body");
            for(var i=0; i<source.length; i++){
                lis[i] = "<li class=\"ui-menu-item\" role=\"menuitem\"><a class=\"ui-corner-all\" tabindex=\"-1\">"+source[i].label+"</a></li>";
            }
            ul.append(lis.join(""));
            this._resizeMenu();
            // setup the rest of the data, and event stuff
            setTimeout(function(){
                self._setupMenuItem.call(self, ul.children("li"), source );
            }, 0);
            input.isFullMenu = true;
        },

        /* incrementally setup the menu items, so the browser can remains responsive when processing thousands of items */
        _setupMenuItem: function( items, source ){
            var self = this,
                itemsChunk = items.splice(0, 500),
                sourceChunk = source.splice(0, 500);
            for(var i=0; i<itemsChunk.length; i++){
                $(itemsChunk[i])
                .data( "item.autocomplete", sourceChunk[i])
                .mouseenter(function( event ) {
                    self.menu.activate( event, $(this));
                })
                .mouseleave(function() {
                    self.menu.deactivate();
                });
            }
            if (items.length > 0){
                setTimeout(function(){
                    self._setupMenuItem.call(self, items, source );
                }, 0);
            }else { // renderFullMenu for the next combobox.
                $(document).dequeue();
            }
        },

        /* overwrite. make the matching string bold */
        _renderItem: function( ul, item ) {
            var label = item.label.replace( new RegExp(
                "(?![^&;]+;)(?!<[^<>]*)(" + $.ui.autocomplete.escapeRegex(this.term) + 
                ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>" );
            return $( "<li></li>" )
                .data( "item.autocomplete", item )
                .append( "<a>" + label + "</a>" )
                .appendTo( ul );
        },

        /* overwrite. to cleanup additional stuff that was added */
        destroy: function() {
            if (this.element.is("SELECT")){
                this.input.remove();
                this.element.removeData().show();
                return;
            }
            // super()
            $.ui.autocomplete.prototype.destroy.call(this);
            // clean up new stuff
            this.element.removeClass( "ui-widget ui-widget-content ui-corner-left" );
            this.button.remove();
        },

        /* overwrite. to swap out and preserve the full menu */ 
        search: function( value, event){
            var input = this.element;
            if (input.isFullMenu){
                this._swapMenu();
                input.isFullMenu = false;
            }
            // super()
            $.ui.autocomplete.prototype.search.call(this, value, event);
        },

        _change: function( event ){
            abc = this;
            if ( !this.selectedItem ) {
                var matcher = new RegExp( "^" + $.ui.autocomplete.escapeRegex( this.element.val() ) + "$", "i" ),
                    match = $.grep( this.options.source, function(value) {
                        return matcher.test( value.label );
                    });
                if (match.length){
                    match[0].option.selected = true;
                }else {
                    // remove invalid value, as it didn't match anything
                    this.element.val( "" );
                    if (this.options.selectElement) {
                        this.options.selectElement.val( "" );
                    }
                }
            }                
            // super()
            $.ui.autocomplete.prototype._change.call(this, event);
        },

        _swapMenu: function(){
            var input = this.element, 
                data = input.data("combobox"),
                tmp = data.menuAll;
            data.menuAll = data.menu.element.hide();
            data.menu.element = tmp;
        },

        /* build the source array from the options of the select element */
        _selectInit: function(){
            var select = this.element.hide(),
            selected = select.children( ":selected" ),
            value = selected.val() ? selected.text() : "";
            this.options.source = select.children( "option[value!='']" ).map(function() {
                return { label: $.trim(this.text), option: this };
            }).toArray();
            var userSelectCallback = this.options.select;
            var userSelectedCallback = this.options.selected;
            this.options.select = function(event, ui){
                ui.item.option.selected = true;
                if (userSelectCallback) userSelectCallback(event, ui);
                // compatibility with jQuery UI's combobox.
                if (userSelectedCallback) userSelectedCallback(event, ui);
            };
            this.options.selectElement = select;
            this.input = $( "<input>" ).insertAfter( select )
                .val( value ).combobox(this.options);
        }
    }
);
})(jQuery);
</script>

0 votes

Génial ! Cela a vraiment accéléré les choses pour moi. Merci !

0 votes

Je voulais utiliser votre implémentation, car elle est parfaite, mais quand j'ai essayé et cliqué sur le bouton, rien ne se passe! Aucun menu n'apparaît! L'autocomplétion fonctionne toujours cependant. Une idée de la raison? Est-ce à cause d'une mise à jour de jquery ui?

7 votes

@dallin le script ci-dessus dépendait de jquery-ui 1.8.x, il a besoin de quelques modifications mineures pour fonctionner avec 1.9.x. Cela fait un moment que je n'ai pas travaillé dessus, mais j'ai posté le code ici github.com/garyzhu/jquery.ui.combobox Je ne l'ai pas testé rigoureusement avec la dernière version de jquery-ui, j'ai juste corrigé les erreurs de javascript évidentes.

20voto

Berro Points 136

J'ai modifié la façon dont les résultats sont renvoyés (dans la fonction source) car la fonction map() me semblait lente. Elle s'exécute plus rapidement pour les grandes listes de sélection (et les plus petites aussi), mais les listes avec plusieurs milliers d'options restent très lentes. J'ai profilé (avec la fonction de profil de firebug) le code original et mon code modifié, et le temps d'exécution est le suivant :

Original : Profilage (372.578 ms, 42307 appels)

Modifié : Profilage (0.082 ms, 3 appels)

Voici le code modifié de la fonction source, vous pouvez voir le code original sur la démo de jquery ui http://jqueryui.com/demos/autocomplete/#combobox. Il y a certainement moyen d'optimiser davantage.

source: function( request, response ) {
    var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" );
    var select_el = this.element.get(0); // obtenir l'élément du DOM
    var rep = new Array(); // tableau de réponse
    // simple boucle pour les options
    for (var i = 0; i < select_el.length; i++) {
        var text = select_el.options[i].text;
        if ( select_el.options[i].value && ( !request.term || matcher.test(text) ) )
            // ajouter l'élément au tableau de résultat
            rep.push({
                label: text, // pas plus de gras
                value: text,
                option: select_el.options[i]
            });
    }
    // envoyer la réponse
    response( rep );
},

J'espère que cela vous aidera.

0 votes

Cette solution renvoie toujours le même ensemble de résultats lorsqu'on utilise la même implémentation pour plus d'une liste déroulante.

0 votes

Peut-être que le code source de jquery-ui a changé au cours des 5 dernières années, mais le "select.get(0);" doit être remplacé par "this.element.get(0);" pour fonctionner.

0 votes

Bonne réponse, mais la boucle for doit avoir select_el.options.length au lieu de select_el.length. J'ai modifié le code.

15voto

Peja Points 131

J'aime la réponse de Berro. Mais comme c'était encore un peu lent (j'avais environ 3000 options dans la sélection), je l'ai modifiée légèrement pour n'afficher que les premiers résultats correspondants. J'ai également ajouté un élément à la fin pour informer l'utilisateur que d'autres résultats sont disponibles et annulé les événements de focus et de sélection pour cet élément.

Voici le code modifié pour les fonctions source et select et ajouté un pour focus :

source: function( demande, réponse ) {
    var correspondance = new RegExp( $.ui.autocomplete.escapeRegex(demande.term), "i" );
    var select_el = select.get(0); // obtenir l'élément du DOM
    var rep = new Array(); // tableau de réponse
    var maxRepSize = 10; // taille de réponse maximale
    // boucle simple pour les options
    for (var i = 0; i < select_el.length; i++) {
        var texte = select_el.options[i].text;
        if ( select_el.options[i].value && ( !demande.term || correspondance.test(texte) ) )
            // ajouter l'élément au tableau de résultat
            rep.push({
                label: texte, // plus en gras
                value: texte,
                option: select_el.options[i]
            });
        if ( rep.length > maxRepSize ) {
            rep.push({
                label: "... plus disponible",
                value: "maxRepSizeReached",
                option: ""
            });
            break;
        }
     }
     // envoyer la réponse
     réponse( rep );
},          
select: function( événement, ui ) {
    if ( ui.item.value == "maxRepSizeReached") {
        return false;
    } else {
        ui.item.option.selected = true;
        self._trigger( "selected", événement, {
            item: ui.item.option
        });
    }
},
focus: function( événement, ui ) {
    if ( ui.item.value == "maxRepSizeReached") {
        return false;
    }
},

0 votes

Bien sûr, les solutions données sont différentes, mais la vôtre a donné la meilleure performance. Merci!

2 votes

C'est une solution géniale. J'ai continué et étendu l'événement _renderMenu de l'autocomplétion car avec les listes déroulantes AutoPostback en asp.net, cela provoque un postback.

0 votes

@iMatoria Praveen monsieur, Aujourd'hui j'ai apporté quelques modifications à votre fichier ajouté et c'est agréable de vous voir également sur ce post... et votre travail Jquery dans Audit Expense est tout simplement génial... Actuellement, je travaille dessus et j'apprends beaucoup grâce à votre code écrit... :) Merci de m'avoir donné la chance de travailler ici... Mais malheureusement, vous êtes parti d'ici... L'apprentissage serait encore plus important si vous étiez ici... :)

11voto

Justin Points 42106

Nous avons constaté la même chose, cependant en fin de compte notre solution a été d'avoir des listes plus petites!

Quand j'ai examiné cela, c'était une combinaison de plusieurs choses :

1) Le contenu de la liste est effacé et reconstruit à chaque fois que la liste est affichée (ou que l'utilisateur tape quelque chose et commence à filtrer la liste). Je pense que c'est principalement inévitable et assez central au fonctionnement de la liste (car il faut retirer des éléments de la liste pour que le filtrage fonctionne).

Vous pourriez essayer de le changer pour qu'il montre et cache les éléments de la liste plutôt que de la reconstruire complètement, mais cela dépendrait de la manière dont votre liste est construite.

L'alternative est de tenter d'optimiser l'effacement / la construction de la liste (voir 2. et 3.).

2) Il y a un retard important lors de l'effacement de la liste. Ma théorie est qu'il est au moins en partie dû au fait que chaque élément de la liste a des données attachées (par la fonction data() de jQuery) - je me souviens que supprimer les données attachées à chaque élément a considérablement accéléré cette étape.

Vous voudrez peut-être examiner des moyens plus efficaces de supprimer les éléments HTML enfants, par exemple Comment rendre jQuery.empty plus de 10 fois plus rapide. Faites attention à ne pas introduire de fuites de mémoire potentielles si vous utilisez des fonctions empty alternatives.

Alternativement, vous pourriez essayer de le modifier pour que les données ne soient pas attachées à chaque élément.

3) Le reste du retard est dû à la construction de la liste - plus spécifiquement, la liste est construite en utilisant une longue chaîne d'instructions jQuery, par exemple :

$("#elm").append(
    $("option").class("sel-option").html(value)
);

Cela semble joli, mais c'est une façon assez inefficace de construire du HTML - une façon beaucoup plus rapide est de construire la chaîne HTML vous-même, par exemple :

$("#elm").html("" + value + "");

Voir Performance des chaînes de caractères : une analyse pour un article assez détaillé sur la manière la plus efficace de concaténer des chaînes de caractères (ce qui est essentiellement ce qui se passe ici).


C'est là que se situe le problème, mais je ne sais honnêtement pas quelle serait la meilleure façon de le résoudre - en fin de compte, nous avons raccourci notre liste d'éléments pour que ce ne soit plus un problème.

En abordant les points 2) et 3), vous pourriez bien constater que les performances de la liste s'améliorent à un niveau acceptable, mais sinon vous devrez aborder le point 1) et essayer de trouver une alternative pour effacer et reconstruire la liste chaque fois qu'elle est affichée.

Étonnamment, la fonction de filtrage de la liste (qui impliquait des expressions régulières assez complexes) n'avait que très peu d'effet sur les performances de la liste déroulante - assurez-vous de ne pas avoir fait quelque chose d'idiot, mais pour nous ce n'était pas le goulot d'étranglement des performances.

0 votes

Merci pour la réponse complète! Cela me donne quelque chose à faire demain :) J'adorerais raccourcir les listes, je ne pense pas qu'une liste déroulante soit entièrement appropriée pour une liste aussi longue, cependant je ne suis pas sûr que cela soit possible.

0 votes

@elwyn - Fais-moi savoir comment ça se passe - C'était l'une de ces choses que je voulais vraiment réparer, mais nous n'avions tout simplement pas le temps de le faire.

1 votes

Est-ce que quelqu'un a optimisé autre chose que ce que Berro a posté ? :)

1voto

soham.m17 Points 303

Ce que j'ai fait, je partage :

Dans le _renderMenu, j'ai écrit ceci :

var isFullMenuAvl = false;
    _renderMenu: function (ul, items) {
                        if (requestedTerm == "**" && !isFullMenuAvl) {
                            var that = this;
                            $.each(items, function (index, item) {
                                that._renderItemData(ul, item);
                            });
                            fullMenu = $(ul).clone(true, true);
                            isFullMenuAvl = true;
                        }
                        else if (requestedTerm == "**") {
                            $(ul).append($(fullMenu[0].childNodes).clone(true, true));
                        }
                        else {
                            var that = this;
                            $.each(items, function (index, item) {
                                that._renderItemData(ul, item);
                            });
                        }
                    }

Ceci est principalement pour le service de demande côté serveur. Mais cela peut être utilisé pour des données locales. Nous stockons requestedTerm et vérifions s'il correspond à ** ce qui signifie que la recherche de menu complet est en cours. Vous pouvez remplacer "**" par "" si vous recherchez le menu complet sans chaîne de recherche. Veuillez me contacter pour tout type de requête. Cela améliore les performances dans mon cas d'au moins 50%.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X